Analyzing WFC3/UVIS G280 Exoplanet Transit Observations#
Learning Goals#
This notebook demonstrates reduction and analysis of exoplanet transit observations of the hot Jupiter HAT-P-41b taken with the WFC3/UVIS G280 grism. By the end of this tutorial, you will be able to do the following for G280 time-series transit observations:
Perform a background subtraction.
Remove cosmic rays spatially and temporally.
Perform trace fitting to extract the 1D time-series of stellar spectra.
Produce light curves for transit fitting.
Table of Contents#
Introduction
1. Imports
2. Downloading Grism Observations from MAST
2.1 Direct Image
2.2 Example G280 Spectrum
2.3 Examining the Time-Series
3. Background Subtraction
4. Cosmic Ray Correction
5. Embedding Subarrays
6. Spectral Trace Fitting
7. G280 Spectral Extraction
8. Extracting 1D Time-Series Stellar Spectra
9. Generating Transit Light Curves
10. Conclusions
Additional Resources
About this Notebook
Citations
Introduction#
Time-series observations of transiting exoplanets can allow us to measure their atmospheric compositions. Hubble has paved the way for such observations, where STIS (G430L, G750L) and WFC3 (G102, G141) served as the workhorse instruments for transit spectroscopy over the past two decades. Recently, however, WFC3’s G280 grism has become popular among the exoplanet community thanks to recent studies demonstrating its usefulness for transit observations (e.g., Wakeford et al. 2020, Lothringer et al. 2022, Boehm et al. 2024). In particular, G280’s blue-optical wavelength range (0.2-0.8
1. Imports#
This notebook assumes you have installed the required libraries as described here.
glob
for finding directories & filesos
for operating system operationsshutil
for file operationstqdm
for loading barsnumpy
for handling array functionsmatplotlib
for plotting dataastropy.io fits
for accessing FITS filesastropy.stats sigma_clipped_stats
for outlier removalastroquery.mast Observations
for downloading data from MASTphotutils.detection DAOStarFinder
for source detection in an image
We also import a custom module g280_transit_tools.py
for G280-specific reduction & analysis tools.
import glob
import os
import shutil
import tqdm
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
from astropy.io import fits
from astropy.stats import sigma_clipped_stats
from astroquery.mast import Observations
from photutils.detection import DAOStarFinder
import g280_transit_tools
%matplotlib inline
/opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
2. Downloading Grism Observations from MAST#
First, we query the observations necessary to complete the tutorial from the Mikulski Archive for Space Telescopes (MAST). Using the G280 grism, GO 15288 observed transits of the giant exoplanet HAT-P-41b, which was the first exoplanet planet transit data set ever taken with G280. The results of this analysis are published in Wakeford et al. (2020). To query only the first visit of the proposal and reject bias frames, we use a wildcard for obs_id
and specify target_name
.
transit_obs = Observations.query_criteria(obs_id='idps01*', target_name='HAT-P-41B')
Next, we retrieve a filtered product list of our observations. For our analysis, we use the _flt.fits
(i.e. calibrated, flat-fielded exposure) files produced using the calwf3
pipeline.
transit_prods = Observations.get_product_list(transit_obs)
transit_prods_filtered = Observations.filter_products(transit_prods, extension=["_flt.fits"], type='S')
Now, we download the _flt.fits
files from MAST.
Observations.download_products(transit_prods_filtered, mrp_only=False, cache=False)
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01oqq_flt.fits to ./mastDownload/HST/idps01oqq/idps01oqq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01orq_flt.fits to ./mastDownload/HST/idps01orq/idps01orq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01osq_flt.fits to ./mastDownload/HST/idps01osq/idps01osq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01otq_flt.fits to ./mastDownload/HST/idps01otq/idps01otq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01ouq_flt.fits to ./mastDownload/HST/idps01ouq/idps01ouq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01ovq_flt.fits to ./mastDownload/HST/idps01ovq/idps01ovq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01oxq_flt.fits to ./mastDownload/HST/idps01oxq/idps01oxq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01oyq_flt.fits to ./mastDownload/HST/idps01oyq/idps01oyq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01ozq_flt.fits to ./mastDownload/HST/idps01ozq/idps01ozq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01p0q_flt.fits to ./mastDownload/HST/idps01p0q/idps01p0q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01p1q_flt.fits to ./mastDownload/HST/idps01p1q/idps01p1q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01p6q_flt.fits to ./mastDownload/HST/idps01p6q/idps01p6q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01p7q_flt.fits to ./mastDownload/HST/idps01p7q/idps01p7q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01p8q_flt.fits to ./mastDownload/HST/idps01p8q/idps01p8q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01p9q_flt.fits to ./mastDownload/HST/idps01p9q/idps01p9q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01paq_flt.fits to ./mastDownload/HST/idps01paq/idps01paq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pcq_flt.fits to ./mastDownload/HST/idps01pcq/idps01pcq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pdq_flt.fits to ./mastDownload/HST/idps01pdq/idps01pdq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01peq_flt.fits to ./mastDownload/HST/idps01peq/idps01peq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pfq_flt.fits to ./mastDownload/HST/idps01pfq/idps01pfq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pgq_flt.fits to ./mastDownload/HST/idps01pgq/idps01pgq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01piq_flt.fits to ./mastDownload/HST/idps01piq/idps01piq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01plq_flt.fits to ./mastDownload/HST/idps01plq/idps01plq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pmq_flt.fits to ./mastDownload/HST/idps01pmq/idps01pmq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pnq_flt.fits to ./mastDownload/HST/idps01pnq/idps01pnq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01poq_flt.fits to ./mastDownload/HST/idps01poq/idps01poq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pqq_flt.fits to ./mastDownload/HST/idps01pqq/idps01pqq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01prq_flt.fits to ./mastDownload/HST/idps01prq/idps01prq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01psq_flt.fits to ./mastDownload/HST/idps01psq/idps01psq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01ptq_flt.fits to ./mastDownload/HST/idps01ptq/idps01ptq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01puq_flt.fits to ./mastDownload/HST/idps01puq/idps01puq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pwq_flt.fits to ./mastDownload/HST/idps01pwq/idps01pwq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01pxq_flt.fits to ./mastDownload/HST/idps01pxq/idps01pxq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q1q_flt.fits to ./mastDownload/HST/idps01q1q/idps01q1q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q2q_flt.fits to ./mastDownload/HST/idps01q2q/idps01q2q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q3q_flt.fits to ./mastDownload/HST/idps01q3q/idps01q3q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q5q_flt.fits to ./mastDownload/HST/idps01q5q/idps01q5q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q6q_flt.fits to ./mastDownload/HST/idps01q6q/idps01q6q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q7q_flt.fits to ./mastDownload/HST/idps01q7q/idps01q7q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q8q_flt.fits to ./mastDownload/HST/idps01q8q/idps01q8q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01q9q_flt.fits to ./mastDownload/HST/idps01q9q/idps01q9q_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qbq_flt.fits to ./mastDownload/HST/idps01qbq/idps01qbq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qcq_flt.fits to ./mastDownload/HST/idps01qcq/idps01qcq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qdq_flt.fits to ./mastDownload/HST/idps01qdq/idps01qdq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qlq_flt.fits to ./mastDownload/HST/idps01qlq/idps01qlq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qmq_flt.fits to ./mastDownload/HST/idps01qmq/idps01qmq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qnq_flt.fits to ./mastDownload/HST/idps01qnq/idps01qnq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qoq_flt.fits to ./mastDownload/HST/idps01qoq/idps01qoq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qpq_flt.fits to ./mastDownload/HST/idps01qpq/idps01qpq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qqq_flt.fits to ./mastDownload/HST/idps01qqq/idps01qqq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qrq_flt.fits to ./mastDownload/HST/idps01qrq/idps01qrq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qsq_flt.fits to ./mastDownload/HST/idps01qsq/idps01qsq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qtq_flt.fits to ./mastDownload/HST/idps01qtq/idps01qtq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01quq_flt.fits to ./mastDownload/HST/idps01quq/idps01quq_flt.fits ...
[Done]
Downloading URL https://mast.stsci.edu/api/v0.1/Download/file?uri=mast:HST/product/idps01qvq_flt.fits to ./mastDownload/HST/idps01qvq/idps01qvq_flt.fits ...
[Done]
Local Path | Status | Message | URL |
---|---|---|---|
str47 | str8 | object | object |
./mastDownload/HST/idps01oqq/idps01oqq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01orq/idps01orq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01osq/idps01osq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01otq/idps01otq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01ouq/idps01ouq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01ovq/idps01ovq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01oxq/idps01oxq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01oyq/idps01oyq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01ozq/idps01ozq_flt.fits | COMPLETE | None | None |
... | ... | ... | ... |
./mastDownload/HST/idps01qmq/idps01qmq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qnq/idps01qnq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qoq/idps01qoq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qpq/idps01qpq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qqq/idps01qqq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qrq/idps01qrq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qsq/idps01qsq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qtq/idps01qtq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01quq/idps01quq_flt.fits | COMPLETE | None | None |
./mastDownload/HST/idps01qvq/idps01qvq_flt.fits | COMPLETE | None | None |
The _flt.fits
files for HAT-P-41b are now in our current working directory under mastDownload
. We organize this data into separate directories for the _flt.fits
and remove the mastDownload
directory.
We define all the subdirectories for MAST, data, direct image, and _flt.fits
(calibrated, clean, and full frame).
cwd = os.getcwd()
download_dir = os.path.join(cwd, 'mastDownload/HST')
data_dir = os.path.join(cwd, 'data')
image_dir = os.path.join(cwd, 'direct_image')
flt_dir = os.path.join(data_dir, 'flt')
flt_clean_dir = os.path.join(data_dir, 'flt_clean')
flt_full_dir = os.path.join(data_dir, 'flt_full')
We create each subdirectory.
for sub_dir in [data_dir, image_dir, flt_dir, flt_clean_dir, flt_full_dir]:
os.makedirs(sub_dir, exist_ok=True)
We move the files to the appropriate subdirectories.
# Retrieve paths in MAST
paths_old = sorted(glob.glob(f'{download_dir}/*/*'))
# Loop through paths
for path_old in paths_old:
basename_old = os.path.basename(path_old)
# Put direct image in appropriate subdirectory
if 'idps01oqq' in basename_old:
sub_dir = image_dir
# Put grism images in appropriate subdirectory
else:
sub_dir = flt_dir
path_new = f'{sub_dir}/{basename_old}'
os.rename(path_old, path_new)
We delete the MAST subdirectory.
if os.path.isdir(download_dir):
shutil.rmtree('mastDownload/')
Now that we have downloaded and organized the relevant files for our time-series analysis, we can analyze them.
2.1 Direct Image#
We retrieve the direct image data of the target, which was taken with the F300X filter. It is common practice for time-series transit observations to take a direct image of the target before taking spectroscopic data.
path_direct_image = f'{image_dir}/idps01oqq_flt.fits'
file_direct_image = os.path.basename(path_direct_image)
direct_image = fits.getdata(path_direct_image)
Now, we plot the direct image using 1 and 99 percentile min and max values, respectively.
vmin, vmax = np.percentile(direct_image, (1, 99))
plt.figure(figsize=[10, 5])
plt.title(f'{file_direct_image} (Direct Image)')
plt.imshow(direct_image, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x7f963f86dc10>

By eye, we can see that our target is the bright source in the center of this image, but let’s find its location more precisely. First, we find the 3-sigma clipped mean, median, and standard deviation
mean, median, std = sigma_clipped_stats(direct_image, sigma=3.0)
Next, we set our DAOStarFinder algorithm to a full width half maximum of 5 and a threshold 1075 times the standard deviation.
find_source = DAOStarFinder(fwhm=5, threshold=1075*std)
Then, we find our sources from the global background (i.e. median) subtracted image.
sources = find_source(direct_image - median)
Finally, we format our columns and return the found sources.
for column in sources.colnames:
sources[column].info.format = '%.8g'
sources
id | xcentroid | ycentroid | sharpness | roundness1 | roundness2 | npix | peak | flux | mag | daofind_mag |
---|---|---|---|---|---|---|---|---|---|---|
int64 | float64 | float64 | float64 | float32 | float64 | int64 | float32 | float32 | float32 | float32 |
1 | 1069.8841 | 437.85493 | 0.97561314 | -0.10247992 | 0.060946854 | 49 | 8806.0215 | 58814.789 | -11.923716 | -0.71761847 |
Our target HAT-P-41b is located around (x,y) = (1070, 438). However, this is in the subarray frame. Therefore, we convert our source coordinates from the subarray frame to the full frame, which is needed later in the analysis. To do this, we use the LTV
keywords from the SCI
extension.
# Extract subarray offsets
x_offset = int(-fits.getval(path_direct_image, 'ltv1', 1))
y_offset = int(-fits.getval(path_direct_image, 'ltv2', 1))
# Extract source coordinates from direct image
source_x_direct = sources['xcentroid'].data[0]
source_y_direct = sources['ycentroid'].data[0]
# Convert coordinates from subarray to full frame
source_x = source_x_direct + x_offset
source_y = source_y_direct + y_offset
print(f'Source Location: (x, y) = {source_x:.4f}, {source_y:.4f}')
Source Location: (x, y) = 2040.8841, 1063.8549
Our source location of HAT-P-41b is within a few hundreths of a pixel to Section 3.1 of Wakeford et al. (2020) (2040.8756, 1063.8825).
2.2 Example G280 Spectrum#
We retrieve the data from one of the spectra in the time-series.
# Retrieve all paths
paths_flt = sorted(glob.glob(f'{flt_dir}/*'))
# Retrieve data from first path
path_grism_image = paths_flt[0]
file_grism_image = os.path.basename(path_grism_image)
grism_image = fits.getdata(path_grism_image)
Now, we plot a grism image using 1 and 99 percentile min and max values, respectively.
vmin, vmax = np.percentile(grism_image, (1, 99))
plt.figure(figsize=[10, 5])
plt.title(f'{file_grism_image} (Grism Image)')
plt.imshow(grism_image, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x7f963d534b90>

G280 spectra have curved spectral traces and multiple overlapping spectral orders. The zeroth order is shown in the center of the image, and is saturated with large diffraction spikes. The spectral traces to the left of the zeroth order are the positive orders, and the traces to the right of the zeroth order are the negative orders. The brightest orders that we can see here are the +1 and -1 orders.
2.3 Examining the Time-Series#
We extract the data from each spectrum, and print the shape.
time_series_grism = np.array([fits.getdata(path) for path in paths_flt], dtype=np.float64)
n_spectra, y_length, x_length = time_series_grism.shape
print(f'Spectra time-series shape {time_series_grism.shape}')
Spectra time-series shape (54, 800, 2100)
Next, we plot a standard deviation stack of the grism images to better understand outliers in the data.
# Standard deviation stack grisms
grism_std = np.std(time_series_grism, axis=0)
# Plot
vmin_std, vmax_std = np.percentile(grism_std, (1, 99))
plt.figure(figsize=[10, 5])
plt.title('WFC3/UVIS G280 HAT-P-41b Standard Deviation')
plt.imshow(grism_std, vmin=vmin_std, vmax=vmax_std, origin='lower', cmap='gray')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x7f963d5ca4d0>

Several hot/bad pixels are present in these images as seen above. The first step in the data reduction process is to clean each exposure in the time-series data and remove these bad pixels, which can affect the precision of our extracted stellar spectra and transit light curves.
3. Background Subtraction#
First, we perform a background subtraction for all of the spectra in the time-series. There are a few ways to do this, but here we will use the publicly available G280 sky frames, which should be downloaded before execution. These sky frames were generated by stacking all public on-orbit science exposures on MAST (as of July 2023) and aggressively masking sources using a modified segmentation map (Pagul et al. 2023). Since our grisms were observed on UVIS2, we download that sky frame.
!curl -O https://www.stsci.edu/~WFC3/grism-resources/uvis-grism-sky-images/G280sky_chip2_flt_v1.0.fits
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 32.0M 100 32.0M 0 0 61.7M 0 --:--:-- --:--:-- --:--:-- 61.8M
Then, we load the UVIS2 G280 sky frame.
g280_sky_full = fits.getdata('G280sky_chip2_flt_v1.0.fits')
We plot the sky with a box around the subarray.
# Plot sky
plt.figure(figsize=[10, 5])
plt.title('UVIS2 G280 Sky')
plt.imshow(g280_sky_full, vmin=0.9, vmax=1.1, origin='lower', cmap='gray')
plt.colorbar()
# Plot box around subarray
plt.vlines(x_offset, y_offset, y_offset + y_length, label='Subarray')
plt.vlines(x_offset + x_length, y_offset, y_offset + y_length)
plt.hlines(y_offset, x_offset, x_offset + x_length)
plt.hlines(y_offset + y_length, x_offset, x_offset + x_length)
plt.legend()
<matplotlib.legend.Legend at 0x7f963c379a90>

Next, we extract the subarray pixels from the sky.
g280_sky = g280_sky_full[y_offset:y_offset+y_length, x_offset:x_offset+x_length]
Then, we scale each sky by each exposure’s median.
time_series_grism_median = np.median(time_series_grism, axis=(1, 2)).reshape(-1, 1, 1) * np.ones_like(time_series_grism)
time_series_g280_sky = g280_sky * time_series_grism_median
Now, we apply background subtraction to each science frame.
time_series_bkg_sub = time_series_grism - time_series_g280_sky
4. Cosmic Ray Correction#
Now, we remove cosmic rays, which can not only hit the detector throughout the course of the hours-long time-series observations, but may also be more frequent in G280 data due to its wide wavelength coverage. It is important to correct for cosmic rays within an exposure (spatially) and also across exposures in the time-series (temporally).
First, we apply the temporal and spatial cosmic ray correction routines using g280_transit_tools.remove_cosmic_rays_time
and g280_transit_tools.remove_cosmic_rays_space
. These functions return the corrected data, and the masks for corrected pixels.
# Temporal correction
time_series_cr_time, mask_outliers_time = g280_transit_tools.remove_cosmic_rays_time(time_series_bkg_sub, n_sigma=4, n_iter=4)
# Spatial correction
time_series_cr_corr, mask_outliers_space = g280_transit_tools.remove_cosmic_rays_space(time_series_cr_time, n_sigma=3, size=3)
0%| | 0/4 [00:00<?, ?it/s]
25%|██▌ | 1/4 [00:02<00:07, 2.37s/it]
50%|█████ | 2/4 [00:04<00:04, 2.35s/it]
75%|███████▌ | 3/4 [00:07<00:02, 2.35s/it]
100%|██████████| 4/4 [00:09<00:00, 2.34s/it]
100%|██████████| 4/4 [00:09<00:00, 2.34s/it]
0%| | 0/54 [00:00<?, ?it/s]
2%|▏ | 1/54 [00:00<00:12, 4.10it/s]
4%|▎ | 2/54 [00:00<00:12, 4.13it/s]
6%|▌ | 3/54 [00:00<00:12, 4.14it/s]
7%|▋ | 4/54 [00:00<00:12, 4.16it/s]
9%|▉ | 5/54 [00:01<00:11, 4.15it/s]
11%|█ | 6/54 [00:01<00:11, 4.17it/s]
13%|█▎ | 7/54 [00:01<00:11, 4.16it/s]
15%|█▍ | 8/54 [00:01<00:11, 4.17it/s]
17%|█▋ | 9/54 [00:02<00:10, 4.17it/s]
19%|█▊ | 10/54 [00:02<00:10, 4.18it/s]
20%|██ | 11/54 [00:02<00:10, 4.17it/s]
22%|██▏ | 12/54 [00:02<00:10, 4.17it/s]
24%|██▍ | 13/54 [00:03<00:09, 4.17it/s]
26%|██▌ | 14/54 [00:03<00:09, 4.16it/s]
28%|██▊ | 15/54 [00:03<00:09, 4.16it/s]
30%|██▉ | 16/54 [00:03<00:09, 4.16it/s]
31%|███▏ | 17/54 [00:04<00:08, 4.16it/s]
33%|███▎ | 18/54 [00:04<00:08, 4.12it/s]
35%|███▌ | 19/54 [00:04<00:08, 4.13it/s]
37%|███▋ | 20/54 [00:04<00:08, 4.15it/s]
39%|███▉ | 21/54 [00:05<00:07, 4.15it/s]
41%|████ | 22/54 [00:05<00:07, 4.15it/s]
43%|████▎ | 23/54 [00:05<00:07, 4.15it/s]
44%|████▍ | 24/54 [00:05<00:07, 4.16it/s]
46%|████▋ | 25/54 [00:06<00:06, 4.17it/s]
48%|████▊ | 26/54 [00:06<00:06, 4.17it/s]
50%|█████ | 27/54 [00:06<00:06, 4.17it/s]
52%|█████▏ | 28/54 [00:06<00:06, 4.17it/s]
54%|█████▎ | 29/54 [00:06<00:06, 4.14it/s]
56%|█████▌ | 30/54 [00:07<00:05, 4.14it/s]
57%|█████▋ | 31/54 [00:07<00:05, 4.15it/s]
59%|█████▉ | 32/54 [00:07<00:05, 4.15it/s]
61%|██████ | 33/54 [00:07<00:05, 4.16it/s]
63%|██████▎ | 34/54 [00:08<00:04, 4.17it/s]
65%|██████▍ | 35/54 [00:08<00:04, 4.16it/s]
67%|██████▋ | 36/54 [00:08<00:04, 4.17it/s]
69%|██████▊ | 37/54 [00:08<00:04, 4.15it/s]
70%|███████ | 38/54 [00:09<00:03, 4.16it/s]
72%|███████▏ | 39/54 [00:09<00:03, 4.16it/s]
74%|███████▍ | 40/54 [00:09<00:03, 4.17it/s]
76%|███████▌ | 41/54 [00:09<00:03, 4.14it/s]
78%|███████▊ | 42/54 [00:10<00:02, 4.12it/s]
80%|███████▉ | 43/54 [00:10<00:02, 4.13it/s]
81%|████████▏ | 44/54 [00:10<00:02, 4.15it/s]
83%|████████▎ | 45/54 [00:10<00:02, 4.15it/s]
85%|████████▌ | 46/54 [00:11<00:01, 4.16it/s]
87%|████████▋ | 47/54 [00:11<00:01, 4.14it/s]
89%|████████▉ | 48/54 [00:11<00:01, 4.14it/s]
91%|█████████ | 49/54 [00:11<00:01, 4.14it/s]
93%|█████████▎| 50/54 [00:12<00:00, 4.15it/s]
94%|█████████▍| 51/54 [00:12<00:00, 4.15it/s]
96%|█████████▋| 52/54 [00:12<00:00, 4.16it/s]
98%|█████████▊| 53/54 [00:12<00:00, 4.16it/s]
100%|██████████| 54/54 [00:12<00:00, 4.17it/s]
100%|██████████| 54/54 [00:12<00:00, 4.16it/s]
We compare the raw 2D spectral image to the spectrum corrected for outliers.
fig, axs = plt.subplots(2, 1, figsize=[10, 10])
# Plot grism image
axs[0].set_title(file_grism_image)
axs0 = axs[0].imshow(grism_image, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
cbar = fig.colorbar(axs0, ax=axs[0])
# Plot clean grism image
axs[1].set_title(f'{file_grism_image} (CR Corrected)')
axs1 = axs[1].imshow(time_series_cr_corr[0], vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
cbar = fig.colorbar(axs1, ax=axs[1])

Our routine sufficiently removed the outliers. All we have left are background stars. Now, we save the cosmic ray corrected data as _c_flt.fits
files.
# Loop through paths
for i, path in tqdm.tqdm(enumerate(paths_flt), total=len(paths_flt)):
# Make a copy of each file
file_c = os.path.basename(path.replace('flt.fits', 'c_flt.fits'))
path_c = f'{flt_clean_dir}/{file_c}'
shutil.copyfile(path, path_c)
# Update the copy
with fits.open(path_c, mode='update') as hdu:
hdu['SCI', 1].data = time_series_cr_corr[i]
0%| | 0/54 [00:00<?, ?it/s]
4%|▎ | 2/54 [00:00<00:02, 19.01it/s]
7%|▋ | 4/54 [00:00<00:02, 19.20it/s]
11%|█ | 6/54 [00:00<00:02, 19.30it/s]
15%|█▍ | 8/54 [00:00<00:02, 19.36it/s]
19%|█▊ | 10/54 [00:00<00:02, 19.34it/s]
22%|██▏ | 12/54 [00:00<00:02, 19.39it/s]
26%|██▌ | 14/54 [00:00<00:02, 19.39it/s]
30%|██▉ | 16/54 [00:00<00:01, 19.28it/s]
33%|███▎ | 18/54 [00:00<00:01, 19.25it/s]
37%|███▋ | 20/54 [00:01<00:01, 19.31it/s]
41%|████ | 22/54 [00:01<00:01, 19.36it/s]
44%|████▍ | 24/54 [00:01<00:01, 19.45it/s]
48%|████▊ | 26/54 [00:01<00:01, 19.44it/s]
52%|█████▏ | 28/54 [00:01<00:01, 19.27it/s]
56%|█████▌ | 30/54 [00:01<00:01, 19.31it/s]
59%|█████▉ | 32/54 [00:01<00:01, 18.93it/s]
63%|██████▎ | 34/54 [00:01<00:01, 18.45it/s]
67%|██████▋ | 36/54 [00:01<00:00, 18.58it/s]
70%|███████ | 38/54 [00:01<00:00, 18.74it/s]
74%|███████▍ | 40/54 [00:02<00:00, 18.86it/s]
78%|███████▊ | 42/54 [00:02<00:00, 18.86it/s]
81%|████████▏ | 44/54 [00:02<00:00, 19.05it/s]
85%|████████▌ | 46/54 [00:02<00:00, 19.17it/s]
89%|████████▉ | 48/54 [00:02<00:00, 19.22it/s]
93%|█████████▎| 50/54 [00:02<00:00, 19.22it/s]
96%|█████████▋| 52/54 [00:02<00:00, 19.26it/s]
100%|██████████| 54/54 [00:02<00:00, 19.26it/s]
100%|██████████| 54/54 [00:02<00:00, 19.15it/s]
5. Embedding Subarrays#
The G280 spectra are taken on UVIS2, but we need to embed this subarray into the full frame image in order to proceed with the spectral extraction. For this step, we use g280_transit_tools.embedsub_uvis
, which is a derivative of wfc3tools.embedsub
. The former function uses header keywords from the _flt.fits
file (i.e. NAXIS
, LTV
), while the latter function requires keywords from the _spt.files
.
# Retrieve clean flt paths
paths_c_flt = sorted(glob.glob(f'{flt_clean_dir}/*'))
# Loop through cleaned paths and embed
for path_c in tqdm.tqdm(paths_c_flt, total=len(paths_c_flt)):
path_c_f = g280_transit_tools.embedsub_uvis(path_c, flt_full_dir)
0%| | 0/54 [00:00<?, ?it/s]
2%|▏ | 1/54 [00:00<00:07, 7.33it/s]
4%|▎ | 2/54 [00:00<00:07, 7.15it/s]
6%|▌ | 3/54 [00:00<00:07, 7.02it/s]
7%|▋ | 4/54 [00:00<00:07, 6.96it/s]
9%|▉ | 5/54 [00:00<00:06, 7.02it/s]
11%|█ | 6/54 [00:00<00:06, 7.05it/s]
13%|█▎ | 7/54 [00:00<00:06, 7.07it/s]
15%|█▍ | 8/54 [00:01<00:06, 7.01it/s]
17%|█▋ | 9/54 [00:01<00:06, 7.12it/s]
19%|█▊ | 10/54 [00:01<00:06, 6.98it/s]
20%|██ | 11/54 [00:01<00:06, 6.89it/s]
22%|██▏ | 12/54 [00:01<00:06, 6.86it/s]
24%|██▍ | 13/54 [00:01<00:05, 6.85it/s]
26%|██▌ | 14/54 [00:02<00:05, 6.75it/s]
28%|██▊ | 15/54 [00:02<00:05, 6.79it/s]
30%|██▉ | 16/54 [00:02<00:05, 6.76it/s]
31%|███▏ | 17/54 [00:02<00:05, 6.76it/s]
33%|███▎ | 18/54 [00:02<00:05, 6.72it/s]
35%|███▌ | 19/54 [00:02<00:05, 6.78it/s]
37%|███▋ | 20/54 [00:02<00:04, 6.83it/s]
39%|███▉ | 21/54 [00:03<00:04, 6.85it/s]
41%|████ | 22/54 [00:03<00:04, 6.88it/s]
43%|████▎ | 23/54 [00:03<00:04, 6.84it/s]
44%|████▍ | 24/54 [00:03<00:04, 6.25it/s]
46%|████▋ | 25/54 [00:03<00:04, 6.12it/s]
48%|████▊ | 26/54 [00:03<00:04, 5.61it/s]
50%|█████ | 27/54 [00:04<00:04, 5.41it/s]
52%|█████▏ | 28/54 [00:04<00:05, 5.04it/s]
54%|█████▎ | 29/54 [00:04<00:04, 5.38it/s]
56%|█████▌ | 30/54 [00:04<00:04, 4.95it/s]
57%|█████▋ | 31/54 [00:04<00:04, 5.11it/s]
59%|█████▉ | 32/54 [00:05<00:04, 4.70it/s]
61%|██████ | 33/54 [00:05<00:04, 4.93it/s]
63%|██████▎ | 34/54 [00:05<00:03, 5.12it/s]
65%|██████▍ | 35/54 [00:05<00:03, 4.97it/s]
67%|██████▋ | 36/54 [00:05<00:03, 4.88it/s]
69%|██████▊ | 37/54 [00:06<00:03, 5.03it/s]
70%|███████ | 38/54 [00:06<00:03, 4.94it/s]
72%|███████▏ | 39/54 [00:06<00:03, 4.65it/s]
74%|███████▍ | 40/54 [00:06<00:02, 4.95it/s]
76%|███████▌ | 41/54 [00:06<00:02, 5.04it/s]
78%|███████▊ | 42/54 [00:07<00:02, 4.71it/s]
80%|███████▉ | 43/54 [00:07<00:02, 4.93it/s]
81%|████████▏ | 44/54 [00:07<00:02, 4.89it/s]
83%|████████▎ | 45/54 [00:07<00:01, 4.80it/s]
85%|████████▌ | 46/54 [00:08<00:01, 4.71it/s]
87%|████████▋ | 47/54 [00:08<00:01, 4.85it/s]
89%|████████▉ | 48/54 [00:08<00:01, 4.87it/s]
91%|█████████ | 49/54 [00:08<00:01, 4.87it/s]
93%|█████████▎| 50/54 [00:08<00:00, 4.99it/s]
94%|█████████▍| 51/54 [00:09<00:00, 4.98it/s]
96%|█████████▋| 52/54 [00:09<00:00, 5.16it/s]
98%|█████████▊| 53/54 [00:09<00:00, 5.07it/s]
100%|██████████| 54/54 [00:09<00:00, 5.10it/s]
100%|██████████| 54/54 [00:09<00:00, 5.63it/s]
We extract the cleaned subarray image and the full frame embedded image.
# Define filenames and paths
file_grism_image_c = file_grism_image.replace('flt', 'c_flt')
file_grism_image_c_f = file_grism_image.replace('flt', 'c_f_flt')
path_grism_image_c = f'{flt_clean_dir}/{file_grism_image_c}'
path_grism_image_c_f = f'{flt_full_dir}/{file_grism_image_c_f}'
# Extract data
grism_image_c = fits.getdata(path_grism_image_c)
grism_image_c_f = fits.getdata(path_grism_image_c_f)
We compare the cleaned subarray image to the full frame embedded image.
fig, axs = plt.subplots(2, 1, figsize=[10, 10])
# Plot clean grism image
axs[0].set_title(f'{file_grism_image_c} (CR Corrected)')
axs0 = axs[0].imshow(grism_image_c, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
cbar = fig.colorbar(axs0, ax=axs[0])
# Plot clean full frame grism image
axs[1].set_title(f'{file_grism_image_c_f} (CR Corrected; Full Frame)')
axs1 = axs[1].imshow(grism_image_c_f, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
cbar = fig.colorbar(axs1, ax=axs[1])

6. Spectral Trace Fitting#
We fit the spectral trace using g280_transit_tools.fit_spectral_trace
. This function uses GRISMCONF, which implements the grism configuration detailed in Pirzkal & Ryan (2017). We download the configuration files required to fit the spectral trace.
!git clone https://github.com/npirzkal/GRISM_WFC3.git
Cloning into 'GRISM_WFC3'...
remote: Enumerating objects: 63, done.
Receiving objects: 1% (1/63)
Receiving objects: 3% (2/63)
Receiving objects: 4% (3/63)
Receiving objects: 6% (4/63)
Receiving objects: 7% (5/63)
Receiving objects: 9% (6/63)
Receiving objects: 11% (7/63)
Receiving objects: 12% (8/63)
Receiving objects: 14% (9/63)
Receiving objects: 15% (10/63)
Receiving objects: 17% (11/63)
Receiving objects: 19% (12/63)
Receiving objects: 20% (13/63)
Receiving objects: 22% (14/63)
Receiving objects: 23% (15/63)
Receiving objects: 25% (16/63)
Receiving objects: 26% (17/63)
Receiving objects: 28% (18/63)
Receiving objects: 30% (19/63)
Receiving objects: 31% (20/63)
Receiving objects: 33% (21/63)
Receiving objects: 34% (22/63)
Receiving objects: 36% (23/63)
Receiving objects: 38% (24/63)
Receiving objects: 39% (25/63)
Receiving objects: 41% (26/63)
Receiving objects: 42% (27/63)
Receiving objects: 44% (28/63)
Receiving objects: 46% (29/63)
Receiving objects: 47% (30/63)
Receiving objects: 49% (31/63)
Receiving objects: 50% (32/63)
Receiving objects: 52% (33/63)
Receiving objects: 53% (34/63)
Receiving objects: 55% (35/63)
Receiving objects: 57% (36/63)
Receiving objects: 58% (37/63)
Receiving objects: 60% (38/63)
Receiving objects: 61% (39/63)
Receiving objects: 63% (40/63)
Receiving objects: 65% (41/63)
Receiving objects: 66% (42/63)
Receiving objects: 68% (43/63)
Receiving objects: 69% (44/63)
Receiving objects: 71% (45/63)
Receiving objects: 73% (46/63)
Receiving objects: 74% (47/63)
Receiving objects: 76% (48/63)
Receiving objects: 77% (49/63)
Receiving objects: 79% (50/63)
Receiving objects: 80% (51/63)
Receiving objects: 82% (52/63)
Receiving objects: 84% (53/63)
Receiving objects: 85% (54/63)
Receiving objects: 87% (55/63)
Receiving objects: 88% (56/63)
Receiving objects: 90% (57/63)
Receiving objects: 92% (58/63)
Receiving objects: 93% (59/63)
remote: Total 63 (delta 0), reused 0 (delta 0), pack-reused 63 (from 1)
Receiving objects: 95% (60/63)
Receiving objects: 96% (61/63)
Receiving objects: 98% (62/63)
Receiving objects: 100% (63/63)
Receiving objects: 100% (63/63), 881.99 KiB | 9.69 MiB/s, done.
Resolving deltas: 0% (0/28)
Resolving deltas: 3% (1/28)
Resolving deltas: 7% (2/28)
Resolving deltas: 10% (3/28)
Resolving deltas: 14% (4/28)
Resolving deltas: 17% (5/28)
Resolving deltas: 21% (6/28)
Resolving deltas: 25% (7/28)
Resolving deltas: 28% (8/28)
Resolving deltas: 32% (9/28)
Resolving deltas: 35% (10/28)
Resolving deltas: 39% (11/28)
Resolving deltas: 42% (12/28)
Resolving deltas: 46% (13/28)
Resolving deltas: 50% (14/28)
Resolving deltas: 53% (15/28)
Resolving deltas: 57% (16/28)
Resolving deltas: 60% (17/28)
Resolving deltas: 64% (18/28)
Resolving deltas: 67% (19/28)
Resolving deltas: 71% (20/28)
Resolving deltas: 75% (21/28)
Resolving deltas: 78% (22/28)
Resolving deltas: 82% (23/28)
Resolving deltas: 85% (24/28)
Resolving deltas: 89% (25/28)
Resolving deltas: 92% (26/28)
Resolving deltas: 96% (27/28)
Resolving deltas: 100% (28/28)
Resolving deltas: 100% (28/28), done.
First, we extract the +1 and -1 orders of one of the cleaned full frame images for the full width of G280 (0.2-0.8
# Define parameters
path_config = 'GRISM_WFC3/UVIS/UVIS_G280_CCD2_V2.conf'
order_p1 = '+1'
order_m1 = '-1'
wl_min = 2000
wl_max = 8000
# Fit +1 spectral trace
trace_x_p1, trace_y_p1, wavelength_p1, sensitivity_p1 = g280_transit_tools.fit_spectral_trace(
path_config, source_x, source_y, order_p1, wl_min, wl_max
)
# Fit -1 spectral trace
trace_x_m1, trace_y_m1, wavelength_m1, sensitivity_m1 = g280_transit_tools.fit_spectral_trace(
path_config, source_x, source_y, order_m1, wl_min, wl_max
)
We plot the sensitivity profiles of each order normalized by the +1 maximum sensitivity.
plt.grid()
plt.title('Sensitivity Profiles')
plt.plot(wavelength_p1, sensitivity_p1/sensitivity_p1.max(), label=f'Order {order_p1}')
plt.plot(wavelength_m1, sensitivity_m1/sensitivity_p1.max(), label=f'Order {order_m1} ')
plt.xlabel('Wavelength (Angstrom)')
plt.ylabel('Normalized Sensitivity')
plt.legend()
<matplotlib.legend.Legend at 0x7f9663959a90>

The +1 order has a higher sensitivity than -1 order. Next, we normalize the exposure.
exptime = fits.getval(path_grism_image_c_f, 'exptime')
spec_data = grism_image_c_f / exptime
Now, we plot the spectral traces using an aperture of +/- 30 pixels outside of the y trace.
# Define aperture windows
n = 30
aperture_min_p1 = trace_y_p1.min() - n
aperture_max_p1 = trace_y_p1.max() + n
aperture_min_m1 = trace_y_m1.min() - n
aperture_max_m1 = trace_y_m1.max() + n
vmin, vmax = np.percentile(spec_data, (1, 99.95))
fig, axs = plt.subplots(2, 1, figsize=[10, 10])
# Plot +1 order
axs[0].set_title(f'{file_grism_image_c_f} (e-/s) Order {order_p1}')
axs0 = axs[0].imshow(spec_data, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
axs[0].plot(trace_x_p1, trace_y_p1, color='r')
axs[0].set_xlim(np.min(trace_x_p1), np.max(trace_x_p1))
axs[0].set_ylim(aperture_min_p1, aperture_max_p1)
cbar = fig.colorbar(axs0, ax=axs[0])
# Plot -1 order
axs[1].set_title(f'{file_grism_image_c_f} (e-/s) Order {order_m1}')
axs1 = axs[1].imshow(spec_data, vmin=vmin, vmax=vmax, origin='lower', cmap='gray')
axs[1].plot(trace_x_m1, trace_y_m1, color='r')
axs[1].set_xlim(np.min(trace_x_m1), np.max(trace_x_m1))
axs[1].set_ylim(aperture_min_m1, aperture_max_m1)
cbar = fig.colorbar(axs1, ax=axs[1])

7. G280 Spectral Extraction#
With our fitted spectral traces, we extract the 1D stellar spectra within a given aperture window using g280_transit_tools.extract_spectrum
. In the example below, we use an aperture window that matches the y-axis scale in the plot above. The exact size of this aperture window is a parameter that we can refine and optimize later on in our analysis.
# Extract +1 order spectrum
counts_p1, counts_err_p1 = g280_transit_tools.extract_spectrum(
path_grism_image_c_f, aperture_min_p1, aperture_max_p1, trace_x_p1
)
# Extract -1 order spectrum
counts_m1, counts_err_m1 = g280_transit_tools.extract_spectrum(
path_grism_image_c_f, aperture_min_m1, aperture_max_m1, trace_x_m1
)
We plot our 1D spectra for the +1 and -1 orders.
plt.grid()
plt.title(f'{file_grism_image_c_f} (1D Spectrum)')
plt.errorbar(wavelength_p1, counts_p1, counts_err_p1, label=f'Order {order_p1}')
plt.errorbar(wavelength_m1, counts_m1, counts_err_m1, label=f'Order {order_m1}')
plt.xlabel('Wavelength (Angstroms)')
plt.ylabel('Counts (e-)')
plt.legend()
<matplotlib.legend.Legend at 0x7f9663757c10>

The +1 order has a factor of ~2 higher throughput compared to the -1 order.
8. Extracting 1D Time-Series Stellar Spectra#
Using the spectral trace fitting parameters, we perform spectral extraction for all of the full frame images in the time series using the previous values for aperture_min
and aperture_max
for g280_transit_tools.extract_spectrum
.
# Retrieve clean embeded flt paths
paths_c_f_flt = sorted(glob.glob(f'{flt_full_dir}/*'))
# Initialize arrays
time_series_counts_p1 = np.zeros((n_spectra, len(trace_x_p1)))
time_series_counts_err_p1 = np.zeros((n_spectra, len(trace_x_p1)))
time_series_counts_m1 = np.zeros((n_spectra, len(trace_x_m1)))
time_series_counts_err_m1 = np.zeros((n_spectra, len(trace_x_m1)))
# Loop over all exposures
for i, path_c_f in tqdm.tqdm(enumerate(paths_c_f_flt), total=n_spectra):
# Extract 1D spectra +1 order
counts_p1, counts_err_p1 = g280_transit_tools.extract_spectrum(
path_c_f, aperture_min_p1, aperture_max_p1, trace_x_p1
)
# Extract 1D spectra -1 order
counts_m1, counts_err_m1 = g280_transit_tools.extract_spectrum(
path_c_f, aperture_min_m1, aperture_max_m1, trace_x_m1
)
# Record data
time_series_counts_p1[i] = counts_p1
time_series_counts_err_p1[i] = counts_err_p1
time_series_counts_m1[i] = counts_m1
time_series_counts_err_m1[i] = counts_err_m1
0%| | 0/54 [00:00<?, ?it/s]
17%|█▋ | 9/54 [00:00<00:00, 89.27it/s]
33%|███▎ | 18/54 [00:00<00:00, 88.33it/s]
50%|█████ | 27/54 [00:00<00:00, 89.04it/s]
67%|██████▋ | 36/54 [00:00<00:00, 89.01it/s]
83%|████████▎ | 45/54 [00:00<00:00, 88.33it/s]
100%|██████████| 54/54 [00:00<00:00, 87.95it/s]
100%|██████████| 54/54 [00:00<00:00, 88.24it/s]
We also extract the midexposure times and hours from mid-transit of the observations.
mjd = np.median([(fits.getval(path, 'expstart'), fits.getval(path, 'expend')) for path in paths_flt], axis=1)
hours = (mjd - mjd.mean()) * 24
Now, we plot the time-series of 1D stellar spectra colored by hours from mid-transit, and the median spectrum colored in black.
# Configure colorbar scale
cmap = cm.viridis
norm = plt.Normalize(hours.min(), hours.max())
colors = cmap(norm(hours))
# Plot spectra +1 order and median
fig, axs = plt.subplots(1, 2, figsize=[12, 5], sharey=True)
axs[0].grid()
axs[0].set_title(f'Time Series of 1D Stellar Spectra Order {order_p1}')
for i, (counts, counts_err) in enumerate(zip(time_series_counts_p1, time_series_counts_err_p1)):
axs0 = axs[0].errorbar(wavelength_p1, counts, counts_err, alpha=0.5, color=colors[i])
counts_median_p1 = np.median(time_series_counts_p1, axis=0)
counts_err_median_p1 = np.median(time_series_counts_err_p1, axis=0)
axs[0].errorbar(wavelength_p1, counts_median_p1, counts_err_median_p1, color='k', label='median')
axs[0].set_xlabel('Wavelength (Angstroms)')
axs[0].set_ylabel('Counts (e-)')
axs[0].legend()
# Plot spectra -1 order and median
axs[1].grid()
axs[1].set_title(f'Time Series of 1D Stellar Spectra Order {order_m1}')
for i, (counts, counts_err) in enumerate(zip(time_series_counts_m1, time_series_counts_err_m1)):
axs1 = axs[1].errorbar(wavelength_m1, counts, counts_err, alpha=0.5, color=colors[i])
counts_median_m1 = np.median(time_series_counts_m1, axis=0)
counts_err_median_m1 = np.median(time_series_counts_err_m1, axis=0)
axs[1].errorbar(wavelength_m1, counts_median_m1, counts_err_median_m1, color='k', label='median')
axs[1].set_xlabel('Wavelength (Angstroms)')
axs[1].set_ylabel('Counts (e-)')
axs[1].legend()
# Set colorbar
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
cbar = plt.colorbar(sm, ax=axs[1])
cbar.set_label(f'Hours from Mid-transit\n$MJD_0$={mjd.min():.3f}')
fig.tight_layout()

9. Generating Light Curves#
With 1D time series stellar spectra, we generate the broadband (i.e. white light) light curves, which is extracted across the full wavelength range of the G280 grism. We generate the light curve using g280_transit_tools.make_light_curve
.
# +1 order
lc_counts_p1, lc_counts_err_p1 = g280_transit_tools.make_light_curve(
wavelength_p1, time_series_counts_p1, wl_min=wl_min, wl_max=wl_max
)
# -1 order
lc_counts_m1, lc_counts_err_m1 = g280_transit_tools.make_light_curve(
wavelength_m1, time_series_counts_m1, wl_min=wl_min, wl_max=wl_max
)
Finally, we plot the normalized light curves.
# Normalize counts by mean of first 10 measurements
lc_counts_p1_norm = lc_counts_p1 / np.mean(lc_counts_p1[0:10])
lc_counts_m1_norm = lc_counts_m1 / np.mean(lc_counts_m1[0:10])
plt.grid()
plt.title('WFC3/UVIS G280 HAT-P-41b Light Curves')
plt.errorbar(hours, lc_counts_p1_norm, lc_counts_err_p1, ls='', marker='o', label=f'Order {order_p1}', alpha=0.5)
plt.errorbar(hours, lc_counts_m1_norm, lc_counts_err_m1, ls='', marker='o', label=f'Order {order_m1}', alpha=0.5)
plt.xlabel(f'Hours from Mid-transit \nMJD={mjd.min():.3f}')
plt.ylabel('Normalized Counts')
plt.legend()
<matplotlib.legend.Legend at 0x7f96430d1750>

The raw white light curves for the +1 and -1 orders look great! We can also use the above function to generate the spectroscopic light curves by changing the wl_min
and wl_max
values.
10. Conclusions#
Thank you for walking through this notebook. With G280 transit observations, you should now be familiar with:
Downloading data from MAST.
Performing cosmic ray correction and background subtraction for the time-series.
Performing trace fitting and spectral extraction for G280 data.
Generating white light curves.
You can now assemble the G280 transmission spectrum for any exoplanet and for any of the spectral orders. Once you have extracted the white light and spectroscopic light curves, you can use various light curve fitting programs (e.g. PACMAN, WFC3 Light Curve Fitting, Eureka!, etc.) to measure the transit depths.
Congratulations, you have completed this tutorial for G280 exoplanet transit data!
Additional Resources#
Below are some additional resources that may be helpful. Please send any questions through the HST Help Desk.
About this Notebook#
Author: Munazza K. Alam, WFC3 & NIRSpec Instrument Teams
Created On: 2025-02-28
Updated On: 2025-03-25; Fred Dauphin, WFC3 Team
Citations#
If you use the following packages for published research, please cite the authors. The links below contain for more information about citing the following: