3. Python Example of Longslit Spectroscopy#
In this note, I show how a longslit spectrum data can be reduced.
Original data: 2-D FITS files (bias, dark, flat, lamp, science image)
Input data for this note: Output (preprocessed data) of 45P Spectroscopic Preprocessing
Final data from this note: 1-D spectrum (flux at each λ)
Note
Let me repeat again: This lecture note is ignoring anything about preprocessing. If preprocessing-related lecture is required, I recommend Matthew Craig’s CCD Data Reduction Guide.
See 45P Spectroscopic Preprocessing for preprocessing the 45P data that will be used in this lecture note.
3.1. About the Data#
The data we will use is the longslit spectrum data of a periodic comet, 45P/Honda–Mrkos–Pajdušáková.
Details of the Observational
Nishi-Harima Astronomical Observatory (NHAO).
Nayuta Telescope (D=2.0m, F/12, Nasmyth)
In 2017 February
FLI PL230-BI CCD (pixel 15µm), 0.34 arcsec/pixel.
Used MALLS (official website in Japanese).
Grating: 150 lines/mm, so-called “Low1” (or just “Low”)
Order cut filter: WG320
λ = 3700–9500Å, total wavelength range = 5700Å
Slit width = 1.6 arcsec
resolution R~600 (@550nm, under slit width 1.2”)
Basic info is written in file headers.
3.2. Postprocessing#
After the preprocessing, there’s still a long way to go. There is no single best way, so we need a lot of interaction with computers. That includes
- Wavelength identification (ID)
First, you cut a small fraction of the comparison lamp image and let the computer calculate the wavelength \(\lambda = \lambda(x)\) for given pixel value
x. This is a “crude version” of the full 2-D map of wavelength (\(\lambda(x, y)\)).- Reidentification (REID)
Then you must check that this function makes sense throughout the whole image. REID checks it. Finally, it will generate the full 2-D map of wavelength (\(\lambda(x, y)\)).
- Aperture trace (aptrace)
The object spectrum cannot be perfectly parallel to the dispersion direction. Aptrace (in IRAF,
APALL) is to find how the source moves along the dispersion direction. If, e.g., x-axis is the dispersion direction, aptrace is the central location of the object’s peak, \(y_\mathrm{obj}(x)\).- Sky subtraction
The sky should be subtracted at each wavelength. This is based on the pixel values at the same λ at different spatial location.
- Aperture sum
We then sum the pixel values within the selected aperture along the spatial direction.
3.3. Preparation#
When you use the code for your data, you must tune each parameter for your own instrument.
# %matplotlib notebook
%config InlineBackend.figure_format = 'retina'
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = 'last_expr'
from matplotlib import pyplot as plt
from pathlib import Path
import numpy as np
from numpy.polynomial.chebyshev import chebfit, chebval
# %matplotlib notebook
# comment above out when you copy this code to your spyder, etc.
# We need to do it in a separate cell. See:
# https://github.com/jupyter/notebook/issues/3385
plt.style.use('default')
plt.rcParams.update({
'font.family': 'latin modern math', 'font.size':12, 'mathtext.fontset':'stix',
'axes.formatter.use_mathtext': True, 'axes.formatter.limits': (-4, 4),
'axes.grid': True, 'grid.color': 'gray', 'grid.linewidth': 0.5,
'xtick.top': True, 'ytick.right': True,
'xtick.direction': 'inout', 'ytick.direction': 'inout',
'xtick.minor.size': 2.0, 'ytick.minor.size': 2.0, # default 2.0
'xtick.major.size': 4.0, 'ytick.major.size': 4.0, # default 3.5
'xtick.minor.visible': True, 'ytick.minor.visible': True
})
from matplotlib import gridspec, rcParams, rc
from matplotlib.widgets import Cursor
from astropy.table import Table, Column
from astropy.io import fits
from astropy.stats import sigma_clip, gaussian_fwhm_to_sigma
from astropy.modeling.models import Gaussian1D, Chebyshev2D
from astropy.modeling.fitting import LevMarLSQFitter
from astropy.nddata import CCDData
from photutils.aperture import RectangularAperture, aperture_photometry
from skimage.feature import peak_local_max
import _tool_visualization as vis
def disable_mplkeymaps():
rc('keymap',
fullscreen='',
home='',
back='',
forward='',
pan='',
zoom='',
save='',
quit='',
grid='',
yscale='',
xscale='',
all_axes=''
)
FONTSIZE = 12 # Change it on your computer if you wish.
rcParams.update({'font.size': FONTSIZE})
#%%
CALPATH = Path('../../Tutorial_Data/45P/calib')
REDPATH = Path('../../Tutorial_Data/45P/reduced')
DISPAXIS = 1 # 1 = line = python_axis_1 // 2 = column = python_axis_0
COMPIMAGE = CALPATH/"comp.fits" # Change directory if needed!
OBJIMAGE = DATAPATH/'mls170213_0025.fits'
LINE_FITTER = LevMarLSQFitter()
# Parameters for IDENTIFY
FITTING_MODEL_ID = 'Chebyshev'
ORDER_ID = 4
NSUM_ID = 10
FWHM_ID = 4 # rough guess of FWHM of lines in IDENTIFY (pixels)
# Parameters for REIDENTIFY
FITTING_MODEL_REID = 'Chebyshev' # 2-D fitting function
ORDER_SPATIAL_REID = 6
ORDER_WAVELEN_REID = 6
STEP_REID = 15 # Reidentification step size in pixels (spatial direction)
NSUM_REID = 10
TOL_REID = 5 # tolerence to lose a line in pixels
# Parameters for APALL (sky fitting and aperture extract after sky subtraction)
## parameters for finding aperture
NSUM_AP = 10
FWHM_AP = 10
STEP_AP = 10 # Recentering step size in pixels (dispersion direction)
## parameters for sky fitting
FITTING_MODEL_APSKY = 'Chebyshev'
ORDER_APSKY = 3
SIGMA_APSKY = 3
ITERS_APSKY = 5
## parameters for aperture tracing
FITTING_MODEL_APTRACE = 'Chebyshev'
ORDER_APTRACE = 3
SIGMA_APTRACE = 3
ITERS_APTRACE = 5
# The fitting is done by SIGMA_APTRACE-sigma ITERS_APTRACE-iters clipped on the
# residual of data.
#%%
lamphdu = fits.open(COMPIMAGE)
objhdu = fits.open(OBJIMAGE)
lampimage = lamphdu[0].data
objimage = objhdu[0].data
if lampimage.shape != objimage.shape:
raise ValueError('lamp and obj images should have same sizes!')
if DISPAXIS == 2:
lampimage = lampimage.T
objimage = objimage.T
elif DISPAXIS != 1:
raise ValueError('DISPAXIS must be 1 or 2 (it is now {:d})'.format(DISPAXIS))
EXPTIME = objhdu[0].header['EXPTIME']
OBJNAME = objhdu[0].header['OBJECT']
# Now python axis 0 (Y-direction) is the spatial axis
# and 1 (X-direciton) is the wavelength (dispersion) axis.
N_SPATIAL, N_WAVELEN = np.shape(lampimage)
N_REID = N_SPATIAL//STEP_REID # No. of reidentification
N_AP = N_WAVELEN//STEP_AP # No. of aperture finding
# ``peak_local_max`` calculates the peak location using maximum filter:
# med1d_max = scipy.ndimage.maximum_filter(med1d, size=10, mode='constant')
# I will use this to show peaks in a primitive manner.
MINSEP_PK = 5 # minimum separation of peaks
MINAMP_PK = 0.01 # fraction of minimum amplitude (wrt maximum) to regard as peak
NMAX_PK = 50
print("setting done!")
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[1], line 69
66 DISPAXIS = 1 # 1 = line = python_axis_1 // 2 = column = python_axis_0
68 COMPIMAGE = CALPATH/"comp.fits" # Change directory if needed!
---> 69 OBJIMAGE = DATAPATH/'mls170213_0025.fits'
70 LINE_FITTER = LevMarLSQFitter()
72 # Parameters for IDENTIFY
NameError: name 'DATAPATH' is not defined