Cleaning Residual 1/f Noise in NIRSpec IFU Products with NSClean#


Notebook Goal#

The goal of this notebook is to generate cleaned IFU (_rate.fits) files by removing residual 1/f noise. These cleaned files will be used as input for the level 3 (Spec3Pipeline) pipeline.

Table of Contents#

    1. Introduction

    1. Import Library

    1. Download the Data

    1. Running Spec2Pipeline without NSClean (Original Data)

    1. Clean up 1/f Noise with NSClean (Default Pipeline Mask)

    • 5.1 Verify the Mask (Default Pipeline Mask)

    • 5.2 Comparing Original vs. Cleaned Data (Default Pipeline Mask)

    1. Clean up 1/f Noise with NSClean (Alternate Mask)

    • 6.1 Verify the Mask (Alternate Mask)

    • 6.2 Comparing Original vs. Cleaned Data (Alternate Mask)

    1. Clean up 1/f Noise with NSClean (Hand-Modified Mask)

    • 7.1 Verify the Mask (Hand-Modified Mask)

    • 7.2 Comparing Original vs. Cleaned Data (Hand-Modified Mask)

    1. Conclusion

  • About the Notebook

1. Introduction #


The JWST NIRSpec instrument has a number of features and characteristics that observers should be aware of when planning observations and interpreting data. One notable feature seen in NIRSpec pipeline products is negative and/or surplus flux in the extracted 1-D spectrum, typically with an irregular wavelength-dependent undulation. The cause of this artifact is correlated noise, known as 1/f noise, from low-level detector thermal instabilities, seen as vertical banding in 2-D count rate images, particularly in exposures of the NRS2 detector. While the IRS2 readout mode reduces this effect, it is not completely eliminated.

To address this issue, the JWST Science Calibration Pipeline has integrated an external package developed by Bernard Rauscher, known as NSClean, within the Spec2Pipeline under NSCleanStep. This algorithm uses dark areas of the detector to fit a background model to the data in Fourier space. It requires an input mask to identify all dark areas of the detector. The more thorough and complete this mask is, the better the background fit.

In this notebook, we will use the NSClean algorithm integrated into the pipeline, utilizing a mask generated on-the-fly with default parameters to remove 1/f noise. In some cases, this mask may not be complete enough/too restrictive for the best possible noise removal. To address this, we demonstrate how one can manually modify the default mask, as well as how to create an alternative mask by adjusting the NSCleanStep parameters. If needed, see the NSClean documentation for some suggestions on manually creating a custom mask.

This notebook utilizes IFU observation of quasar XID2028 with grating/filter G140H/F100LP as part of JWST Early Release Science program ERS-1335 observation 4, as an example.

2. Import Library #


# ---------- Set CRDS environment variables ----------
import os
import jwst
os.environ['CRDS_CONTEXT'] = 'jwst_1210.pmap'
os.environ['CRDS_PATH'] = os.environ['HOME']+'/crds_cache'
os.environ['CRDS_SERVER_URL'] = 'https://jwst-crds.stsci.edu'
print(f'CRDS cache location: {os.environ["CRDS_PATH"]}')

print("JWST Calibration Pipeline Version={}".format(jwst.__version__))
# print("Current Operational CRDS Context = {}".format(crds.get_default_context()))
CRDS cache location: /home/runner/crds_cache
JWST Calibration Pipeline Version=1.13.4
# ------ General Imports ------
import numpy as np
import time as tt
import logging
import warnings

# ------ JWST Calibration Pipeline Imports ------
from jwst.pipeline.calwebb_spec2 import Spec2Pipeline

# ------ Plotting/Stats Imports ------
from matplotlib import pyplot as plt
from astropy.io import fits

from utils import get_jwst_file, plot_dark_data, plot_cleaned_data, plot_spectra

# Hide all log and warning messages.
logging.disable(logging.ERROR)
warnings.simplefilter("ignore", RuntimeWarning)

3. Download the Data #


The input data for this notebook features an IFU observation of quasar XID2028 with grating/filter G140H/F100LP. The dataset is part of the JWST Early Release Science program ERS-1335, specifically observation 4. It consists of 9 integrations (9 dither points) with 16 groups each. This notebook focuses on the second dithered exposure (00002) as an example. However, it’s important to note that before proceeding to the Spec3Pipeline, all exposures must first be processed through the Spec2Pipeline.

# Define a downloads directory.
mast_products_dir = "./mast_products/"
# Check if the directory exists.
if not os.path.exists(mast_products_dir):
    # Create the directory if it doesn't exist.
    os.makedirs(mast_products_dir)
# This notebook focuses on the second dithered exposure.
obs_ids = ["jw01335004001_03101_00002"]
detectors = [1, 2]  # Both Detectors NRS1 and NRS2.

# Specify the countrate products.
rate_names = []

for obs_id in obs_ids:
    for detector in detectors:
        rate_names.append(f"{obs_id}_nrs{detector}_rate.fits")

# Download all the FITS files.
for name in rate_names:
    print(f"Downloading {name}")
    get_jwst_file(name, mast_api_token=None, save_directory=mast_products_dir)
Downloading jw01335004001_03101_00002_nrs1_rate.fits
Downloading jw01335004001_03101_00002_nrs2_rate.fits

4. Running Spec2Pipeline without NSClean (Original Data) #


The cell below executes the Spec2Pipeline, explicitly skipping the NSClean step during processing. The level 2 products generated will serve as a reference point to illustrate how the countrate images and final extracted spectra appear without the removal of 1/f noise.

# Set up directory for running the pipeline without NSClean.
stage2_nsclean_skipped_dir = "./stage2_nsclean_skipped/"
if not os.path.exists(stage2_nsclean_skipped_dir):
    os.makedirs(stage2_nsclean_skipped_dir)  # Create the directory if it doesn't exist.
# Original data (no NSClean Applied).
# Estimated run-time: 132 minutes (may vary).
start = tt.time()

for i in rate_names:
    print(f"Processing {i}...")

    Spec2Pipeline.call(
        mast_products_dir + i,
        save_results=True,
        steps={
            "nsclean": {
                "skip": True
            },  # Removes correlated read noise (1/f noise) from NIRSpec images.
        },
        output_dir=stage2_nsclean_skipped_dir,
    )

    print(f"Saved {i[:-9]}" + "cal.fits")
    print(f"Saved {i[:-9]}" + "x1d.fits")


end = tt.time()
print("Run time: ", round(end - start, 1) / 60.0, " min")
Processing jw01335004001_03101_00002_nrs1_rate.fits...
Saved jw01335004001_03101_00002_nrs1_cal.fits
Saved jw01335004001_03101_00002_nrs1_x1d.fits
Processing jw01335004001_03101_00002_nrs2_rate.fits...
Saved jw01335004001_03101_00002_nrs2_cal.fits
Saved jw01335004001_03101_00002_nrs2_x1d.fits
Run time:  14.358333333333333  min

5. Clean up 1/f Noise with NSClean (Default Pipeline Mask) #


If a user-supplied mask file is not provided to the NSClean step in the Spec2Pipeline, the pipeline will generate a mask based on default parameters. This mask will identify any pixel that is unilluminated. That is, the mask must contain True and False values, where True indicates that the pixel is dark, and False indicates that the pixel is illuminated (not dark).

By default, the pipeline marks the following detector areas as illuminated, non-dark areas (False):

  • Pixels designated as science areas for IFU data.

  • Traces from failed-open MSA shutters.

  • 5-sigma outliers (default value).

  • Any pixel set to NaN in the rate data.

To tune the outlier detection in the mask, try modifying the n_sigma parameter (explored in the next section). A higher value will identify fewer outliers. A lower value will identify more.

The default generated mask is saved and analyzed below.

# Set up directory for running NSClean with default parameters.
stage2_nsclean_default_dir = "./stage2_nsclean_default/"
if not os.path.exists(stage2_nsclean_default_dir):
    os.makedirs(stage2_nsclean_default_dir)  # Create the directory if it doesn't exist.
# 1/f noise cleaned data (default NSClean pipeline mask).
# Estimated run time: 105 minutes (may vary).
start = tt.time()

for i in rate_names:
    print(f"Processing {i}...")

    Spec2Pipeline.call(
        mast_products_dir + i,
        save_results=True,
        steps={
            "nsclean": {
                "skip": False,
                "save_mask": True,
                "save_results": True,
            },  # Removes correlated read noise (1/f noise) from NIRSpec images.
        },
        output_dir=stage2_nsclean_default_dir,
    )

    print(f"Saved {i[:-9]}" + "mask.fits")
    print(f"Saved {i[:-9]}" + "nsclean.fits")
    print(f"Saved {i[:-9]}" + "cal.fits")
    print(f"Saved {i[:-9]}" + "x1d.fits")


end = tt.time()
print("Run time: ", round(end - start, 1) / 60.0, " min")
Processing jw01335004001_03101_00002_nrs1_rate.fits...
Saved jw01335004001_03101_00002_nrs1_mask.fits
Saved jw01335004001_03101_00002_nrs1_nsclean.fits
Saved jw01335004001_03101_00002_nrs1_cal.fits
Saved jw01335004001_03101_00002_nrs1_x1d.fits
Processing jw01335004001_03101_00002_nrs2_rate.fits...
Saved jw01335004001_03101_00002_nrs2_mask.fits
Saved jw01335004001_03101_00002_nrs2_nsclean.fits
Saved jw01335004001_03101_00002_nrs2_cal.fits
Saved jw01335004001_03101_00002_nrs2_x1d.fits
Run time:  17.451666666666664  min
Warning:

In some situations, the NSClean step may fail to find a fit to the background noise. This failure may occur if the mask does not contain enough dark data (marked True). In particular, every column in the mask except for the first and last 4 columns must contain some pixels marked True. The background fitting procedure considers each column, one at a time, so it will crash if there is no data in a column to fit. If failure occurs, check that your mask in the image below has at least some True values in every column.

5.1 Verify the Mask (Default Pipeline Mask) #


Check the mask against the rate data to make sure it keeps only dark areas of the detector.

Note that there are still some remaining illuminated areas, primarily due to transient artifacts like cosmic rays and snowballs.

# Plot the rate data with masked areas blocked.

# List of on-the-fly built masks from the pipeline.
nsclean_default_masks = [
    stage2_nsclean_default_dir + "jw01335004001_03101_00002_nrs1_mask.fits",
    stage2_nsclean_default_dir + "jw01335004001_03101_00002_nrs2_mask.fits",
]

# Plot each associated set of rateint data and mask file.
for rate_file, mask_file in zip(rate_names, nsclean_default_masks):
    plot_dark_data(mast_products_dir + rate_file, mask_file, layout="columns", scale=9)
../../_images/3123635ba0f5b68097eb297b7dcd781c5d258dc81918d38f8ad16cab757bce48.png ../../_images/43e7c772b5b41ab5ed6eac6bccd62738229a88d9d731d2b730f61d21aa17fb3c.png

5.2 Comparing Original vs. Cleaned Data (Default Pipeline Mask) #


We can now compare the cleaned data (with the default pipeline mask) to the original rate file and verify that the 1/f noise has been reduced.

In many cases, the cleaning process introduces new artifacts to the rate file. These should be carefully examined and weighed against the benefits of noise reduction. If transient artifacts, like snowballs, are interfering with the cleaning process, it may be beneficial to manually edit the mask to remove these areas from consideration in the background fit. To do so, try varying the outlier detection threshold or editing specific pixels in the mask array directly (explored in the next few sections). Otherwise, refer to the NSClean documentation for additional suggestions on manual editing.

Note that in the images below, there are scattered values with large relative differences from the original rate file (shown in the relative difference image below). These are artifacts of the cleaning process.

There are also broader low-level residual background effects (shown in the relative difference image on the right, below, with scattered outliers, identified with sigma clipping, hidden by masking). These include the background patterns we are trying to remove: the 1/f noise variations in the dispersion direction and the picture frame effect at the top and bottom of the frame (for full-frame data). However, there may also be low-level artifacts introduced by over-fitting the dark data in the cleaning process.

Check both residual images carefully to understand the impact of the cleaning process on your data.

# Plot the original and cleaned data, as well as a residual map.

cleaned_default_masks = [
    stage2_nsclean_default_dir + "jw01335004001_03101_00002_nrs1_nsclean.fits",
    stage2_nsclean_default_dir + "jw01335004001_03101_00002_nrs2_nsclean.fits",
]

# Plot each associated set of rateint data and cleaned file.
for rate_file, cleaned_file in zip(rate_names, cleaned_default_masks):
    plot_cleaned_data(
        mast_products_dir + rate_file, cleaned_file, layout="columns", scale=9
    )
../../_images/44d93e60ae58822c5b42754a67366188b8c82590450f6e173848cfa090c516d6.png ../../_images/f4b7b86c11cbd8afe2460068867bcbbc47639452a4fd9cf8f1bb04b548323245.png

Compare the extracted spectrum from the cleaned data to the spectrum extracted from the original rate file.

# 1D extracted spectra.
x1d_nsclean_skipped = [
    stage2_nsclean_skipped_dir + "jw01335004001_03101_00002_nrs1_x1d.fits",
    stage2_nsclean_skipped_dir + "jw01335004001_03101_00002_nrs2_x1d.fits",
]
x1d_nsclean_default = [
    stage2_nsclean_default_dir + "jw01335004001_03101_00002_nrs1_x1d.fits",
    stage2_nsclean_default_dir + "jw01335004001_03101_00002_nrs2_x1d.fits",
]

# Wavelength region of interest.
wavelength_range = {"nrs1": [1.15, 1.25], "nrs2": [1.65, 1.75]}
for original, cleaned in zip(x1d_nsclean_skipped, x1d_nsclean_default):
    plot_spectra(
        [original, cleaned], scale_percent=4, wavelength_range=wavelength_range
    )
../../_images/4a6cc1c84c3cc29c5f2fe6905807b8b62c559846cc6aa666a6d4271dcf7815af.png ../../_images/2f4f3070552299214ad7d6d899d6d15f09764d60e989cbc4fb399f9dc837d6e5.png

Notes:

  • The portion of the spectrum near 1.2um for NRS1 and 1.7um for NRS2, the excess flux due to 1/f noise is reduced.

  • There are several spikes in the difference between the cleaned and original spectra. These correspond to the scattered artifacts introduced by the cleaning process, above.

6. Clean up 1/f Noise with NSClean (Alternate Mask) #


For some data sets, masking the entire science region may excessively mask dark areas of the detector that could be used to improve the background fit. Excessive masking can introduce some high frequency noise in the cleaning process that appears as vertical striping over the spectral traces. Also, for some data sets, there may be several illuminated regions of the detector that are not masked by the IFU slice bounding boxes. This may include transient artifacts like cosmic rays or glow from saturated sources.

In some cases, it may be beneficial to build the mask with an alternate algorithm. Here, we do not use the bounding boxes and instead iteratively mask any data more than 1 sigma above the background. For bright sources, this might leave more dark data between the spectral traces and may improve the background fit.

Note, however, that excessive cleaning may impact the continuum level for the spectrum, if too much or too little illuminated data is included in the mask. Again, the generated mask and output spectra should be carefully examined to weigh the benefits of cleaning against the impact on the spectra.

To tune the illumination detection in this mask, try modifying the n_sigma parameter below. A higher value will identify less illumination. A lower value will identify more.

# Set up directory for running NSClean with user-supplied mask.
stage2_nsclean_alternate_dir = "./stage2_nsclean_alternate/"
if not os.path.exists(stage2_nsclean_alternate_dir):
    os.makedirs(
        stage2_nsclean_alternate_dir
    )  # Create the directory if it doesn't exist.
# 1/f noise cleaned data (alternate NSClean pipeline mask).
# Estimated run time: 87 minutes (may vary).
start = tt.time()

for indx, i in enumerate(rate_names):
    print(f"Processing {i}... ")

    Spec2Pipeline.call(
        mast_products_dir + i,
        save_results=True,
        steps={
            "nsclean": {
                "skip": False,
                "save_mask": True,
                "n_sigma": 1,
                "mask_spectral_regions": False,
                "save_results": True,
            },  # Removes correlated read noise (1/f noise) from NIRSpec images.
        },
        output_dir=stage2_nsclean_alternate_dir,
    )

    print(f"Saved {i[:-9]}" + "mask.fits")
    print(f"Saved {i[:-9]}" + "nsclean.fits")
    print(f"Saved {i[:-9]}" + "cal.fits")
    print(f"Saved {i[:-9]}" + "x1d.fits")

end = tt.time()
print("Run time: ", round(end - start, 1) / 60.0, " min")
Processing jw01335004001_03101_00002_nrs1_rate.fits... 
Saved jw01335004001_03101_00002_nrs1_mask.fits
Saved jw01335004001_03101_00002_nrs1_nsclean.fits
Saved jw01335004001_03101_00002_nrs1_cal.fits
Saved jw01335004001_03101_00002_nrs1_x1d.fits
Processing jw01335004001_03101_00002_nrs2_rate.fits... 
Saved jw01335004001_03101_00002_nrs2_mask.fits
Saved jw01335004001_03101_00002_nrs2_nsclean.fits
Saved jw01335004001_03101_00002_nrs2_cal.fits
Saved jw01335004001_03101_00002_nrs2_x1d.fits
Run time:  15.120000000000001  min

6.1 Verify the Mask (Alternate Mask) #


Check the mask against the rate data to make sure it keeps only dark areas of the detector.

# Plot the rate data with masked areas blocked.

# List of on-the-fly built masks from the pipeline.
nsclean_alternate_masks = [
    stage2_nsclean_alternate_dir + "jw01335004001_03101_00002_nrs1_mask.fits",
    stage2_nsclean_alternate_dir + "jw01335004001_03101_00002_nrs2_mask.fits",
]

# Plot each associated set of rateint data and mask file.
for rate_file, mask_file in zip(rate_names, nsclean_alternate_masks):
    plot_dark_data(mast_products_dir + rate_file, mask_file, layout="columns", scale=9)
../../_images/867e84a9e38c4dbeba3c99e3d2535a9ede55e358eb30f56dc1d07d2c169c6bee.png ../../_images/ba47140356913a443d689b6d9f44d79a668864e991a814de527cbcc5ad82975a.png

6.2 Comparing Original vs. Cleaned Data (Alternate Mask) #


# Plot the original and cleaned data, as well as a residual map.

cleaned_alternate_masks = [
    stage2_nsclean_alternate_dir + "jw01335004001_03101_00002_nrs1_nsclean.fits",
    stage2_nsclean_alternate_dir + "jw01335004001_03101_00002_nrs2_nsclean.fits",
]

# Plot each associated set of rateint data and cleaned file.
for rate_file, cleaned_file in zip(rate_names, cleaned_alternate_masks):
    plot_cleaned_data(
        mast_products_dir + rate_file, cleaned_file, layout="columns", scale=9
    )
../../_images/865b43a294228425dd99fd6038f6878713fedb7550862081e029dde4aa76d828.png ../../_images/4a150ab385d2742a655de0f4648d3969f604b356fe2b8ecb92827828e19ea046.png

Compare the extracted spectrum from the cleaned data to the spectrum extracted from the original rate file.

x1d_nsclean_alternate = [
    stage2_nsclean_alternate_dir + "jw01335004001_03101_00002_nrs1_x1d.fits",
    stage2_nsclean_alternate_dir + "jw01335004001_03101_00002_nrs2_x1d.fits",
]

# Wavelength region of interest.
wavelength_range = {"nrs1": [1.15, 1.25], "nrs2": [1.65, 1.75]}
for original, cleaned in zip(x1d_nsclean_skipped, x1d_nsclean_alternate):
    plot_spectra(
        [original, cleaned], scale_percent=4, wavelength_range=wavelength_range
    )
../../_images/ad7dfb65d79bf53cb155276495d3c31b75b46e57dad0a9e2a3431b025ba6e0ee.png ../../_images/2f4fa17b7b3f267f9e9326567cc2c5876249b455f9b7830d66f230d80caa1117.png

Notes:

  • The overall continuum level for the spectrum on NRS1 has changed after cleaning. This is because sigma-clipping, unlike the default method, only masks the bright outliers, rather than the entire science region. Consequently, some sky regions are included in the background model that gets subtracted from the data, which can result in changes to the continuum level.

  • The portion of the spectrum near 1.7um for NRS2, the excess flux due to 1/f noise is reduced.

  • There are several spikes in the difference between the cleaned and original spectra. These correspond to the scattered artifacts introduced by the cleaning process, above.

7. Clean up 1/f Noise with NSClean (Hand-Modified Mask) #


In certain scenarios, manual generation of a mask may be required. Here, we present one approach to manually modify the mask (excluding some large snowballs in NRS1/NRS2) starting with the default mask output from the pipeline. It is worth noting that the mask modified using this method may not necessarily outperform the two previous options.

# Set up directory for running NSClean with user-supplied mask.
stage2_nsclean_modified_dir = "./stage2_nsclean_modified/"
if not os.path.exists(stage2_nsclean_modified_dir):
    # Create the directory if it doesn't exist.
    os.makedirs(stage2_nsclean_modified_dir)
# Hand-modify certain mask regions
# Snowballs in NRS1/2.

# Define the list to store paths of modified masks.
nsclean_modified_masks = []

# Iterate through the list of original masks.
for mask in nsclean_default_masks:
    # New mask file name.
    output_file = os.path.basename(mask)[:-5] + "_modified.fits"

    # Open the FITS file.
    with fits.open(mask) as hdul:
        # Extract the mask data from the science extension.
        mask_data = hdul["SCI"].data.copy()  # Make a copy.

        if "nrs1" in mask:
            # Mask Snowballs.
            mask_data[10:50, 180:215] = False
            mask_data[560:580, 450:465] = False
            mask_data[1720:1810, 1150:1240] = False
            mask_data[1540:1580, 535:580] = False
            mask_data[1865:1910, 725:770] = False
        else:
            mask_data[200:250, 130:190] = False
            mask_data[1400:1420, 40:80] = False
            mask_data[1700:1730, 150:180] = False
            mask_data[630:710, 1100:1170] = False
            mask_data[520:570, 1770:1820] = False

        # Update the data within the science extension.
        hdul["SCI"].data = mask_data
        # Save the modified FITS file.
        output_path = os.path.join(stage2_nsclean_modified_dir, output_file)
        hdul_modified = hdul.copy()  # Make a copy.
        hdul_modified.writeto(output_path, overwrite=True)
        nsclean_modified_masks.append(output_path)
        print(f"Saved modified mask as: {output_path}")
Saved modified mask as: ./stage2_nsclean_modified/jw01335004001_03101_00002_nrs1_mask_modified.fits
Saved modified mask as: ./stage2_nsclean_modified/jw01335004001_03101_00002_nrs2_mask_modified.fits

7.1 Verify the Mask (Hand-Modified Mask) #


Check the mask against the rate data to make sure it keeps only dark areas of the detector.

# Plot the rate data with masked areas blocked.

# List of modified masks for the pipeline.
nsclean_modified_masks = [
    stage2_nsclean_modified_dir + "jw01335004001_03101_00002_nrs1_mask_modified.fits",
    stage2_nsclean_modified_dir + "jw01335004001_03101_00002_nrs2_mask_modified.fits",
]

# Plot each associated set of rateint data and mask file.
for rate_file, mask_file in zip(rate_names, nsclean_modified_masks):
    plot_dark_data(mast_products_dir + rate_file, mask_file, layout="columns", scale=9)
../../_images/479a7fb7df018dfdb03bd90a4dc239347910de6c3bbd8dbd61a12048fefb8fa6.png ../../_images/fa634eb63a9a06bc61ec40db6b463f784f4a535d5ca48087b4e532d528b800b4.png

# 1/f noise cleaned data (user-supplied mask).
# Estimated run time: 87 minutes (may vary).
start = tt.time()

for indx, i in enumerate(rate_names):
    print(f"Processing {i}... ")

    Spec2Pipeline.call(
        mast_products_dir + i,
        save_results=True,
        steps={
            "nsclean": {
                "skip": False,
                "save_mask": True,
                "save_results": True,
                "user_mask": nsclean_modified_masks[indx],
            },  # Removes correlated read noise (1/f noise) from NIRSpec images.
        },
        output_dir=stage2_nsclean_modified_dir,
    )

    print(f"Saved {i[:-9]}" + "mask.fits")
    print(f"Saved {i[:-9]}" + "nsclean.fits")
    print(f"Saved {i[:-9]}" + "cal.fits")
    print(f"Saved {i[:-9]}" + "x1d.fits")


end = tt.time()
print("Run time: ", round(end - start, 1) / 60.0, " min")
Processing jw01335004001_03101_00002_nrs1_rate.fits... 
Saved jw01335004001_03101_00002_nrs1_mask.fits
Saved jw01335004001_03101_00002_nrs1_nsclean.fits
Saved jw01335004001_03101_00002_nrs1_cal.fits
Saved jw01335004001_03101_00002_nrs1_x1d.fits
Processing jw01335004001_03101_00002_nrs2_rate.fits... 
Saved jw01335004001_03101_00002_nrs2_mask.fits
Saved jw01335004001_03101_00002_nrs2_nsclean.fits
Saved jw01335004001_03101_00002_nrs2_cal.fits
Saved jw01335004001_03101_00002_nrs2_x1d.fits
Run time:  14.573333333333332  min

7.2 Comparing Original vs. Cleaned Data (Hand-Modified Mask) #


# Plot the original and cleaned data, as well as a residual map.

cleaned_modified_masks = [
    stage2_nsclean_modified_dir + "jw01335004001_03101_00002_nrs1_nsclean.fits",
    stage2_nsclean_modified_dir + "jw01335004001_03101_00002_nrs2_nsclean.fits",
]

# Plot each associated set of rateint data and cleaned file.
for rate_file, cleaned_file in zip(rate_names, cleaned_modified_masks):
    plot_cleaned_data(
        mast_products_dir + rate_file, cleaned_file, layout="columns", scale=9
    )
../../_images/f97b9b6f0b7c24e2434f98aeff6722a0668f89f4fa8c33d3680b1a49b0160a65.png ../../_images/763ee5c09f6d11315f46827ca31741da0270591bf1503dfc267eb4994f6e8126.png

Compare the extracted spectrum from the cleaned data to the spectrum extracted from the original rate file.

x1d_nsclean_modified = [
    stage2_nsclean_modified_dir + "jw01335004001_03101_00002_nrs1_x1d.fits",
    stage2_nsclean_modified_dir + "jw01335004001_03101_00002_nrs2_x1d.fits",
]

# Wavelength region of interest
wavelength_range = {"nrs1": [1.15, 1.25], "nrs2": [1.65, 1.75]}
for original, cleaned in zip(x1d_nsclean_skipped, x1d_nsclean_modified):
    plot_spectra(
        [original, cleaned], scale_percent=4, wavelength_range=wavelength_range
    )
../../_images/0013a6ccdc9b3b4554541e1a9ea18a09f03f19187ab01ff7f59924baf6ae009c.png ../../_images/eb29f22870d30afb2e21b5bb870b3587188adeab19c66717e888642f0cc40ec4.png

Notes:

  • Even when masking the handful of snowballs we encountered, the spectra remain similar to those cleaned with the default mask.

  • The portion of the spectrum near 1.2um for NRS1 and 1.7um for NRS2, the excess flux due to 1/f noise is reduced.

  • There are several spikes in the difference between the cleaned and original spectra. These correspond to the scattered artifacts introduced by the cleaning process, above.

8. Conclusion #


The final plots below show the countrate images and the resulting 1D extracted spectra side-by-side to compare the different cleaning methods: the original (no NSClean applied), the cleaned countrate image (with the default pipeline mask), the cleaned countrate image (with an alternate pipeline mask), and finally, the cleaned countrate image (with the hand-modified mask).

Please note that the results presented in this notebook may vary for different datasets (e.g., targets of different brightness, spatial extent, etc.). Users are encouraged to explore NSClean using different masking methods to determine the optimal results.

The output from the cleaning algorithm is now ready for further processing. The (_cal.fits) files produced by the above Spec2Pipeline run may be used as input to the Spec3Pipeline, for generating final combined spectral cubes and extracted spectra.

# Not cleaned vs. cleaned (default mask) vs. cleaned (alternate mask) rate data
original_rate_data = [
    fits.open(mast_products_dir + rate_name)[1].data for rate_name in rate_names
]
cleaned_rate_default_data = [
    fits.open(cleaned_default_mask)[1].data
    for cleaned_default_mask in cleaned_default_masks
]
cleaned_rate_alternate_data = [
    fits.open(cleaned_alternate_mask)[1].data
    for cleaned_alternate_mask in cleaned_alternate_masks
]
cleaned_rate_modified_data = [
    fits.open(cleaned_modified_mask)[1].data
    for cleaned_modified_mask in cleaned_modified_masks
]

# For plotting visualization
for data_list in [
    original_rate_data,
    cleaned_rate_default_data,
    cleaned_rate_alternate_data,
    cleaned_rate_modified_data,
]:
    for data in data_list:
        data[np.isnan(data)] = 0

# Original vs. cleaned data (with default mask)
fig, axs = plt.subplots(2, 4, figsize=(25, 12))

# Set y-axis titles and plot the data
titles = [
    "Original Rate Data",
    "Cleaned Rate Data (Default Mask)",
    "Cleaned Rate Data (Alternate Mask)",
    "Cleaned Rate Data (Hand-Modified Mask)",
]
for i, (data_list, title) in enumerate(
    zip(
        [
            original_rate_data,
            cleaned_rate_default_data,
            cleaned_rate_alternate_data,
            cleaned_rate_modified_data,
        ],
        titles,
    )
):
    for j, data in enumerate(data_list):
        ax = axs[j, i]
        ax.set_title(f'{title} \n {"NRS1" if j == 0 else "NRS2"}', fontsize=12)
        im = ax.imshow(data, origin="lower", clim=(-1e-3, 1e-2))
        fig.colorbar(im, ax=ax, pad=0.05, shrink=0.7, label="DN/s")
        ax.set_xlabel("Pixel Column", fontsize=10)
        ax.set_ylabel("Pixel Row", fontsize=10)

plt.tight_layout()
plt.show()
../../_images/97cf352053a24717590fc775b138a52161cfb24950cf8f7c88e6f3dc5f56d442.png
# Final Comparison

# Wavelength region of interest
wavelength_range = {"nrs1": [1.15, 1.25], "nrs2": [1.65, 1.75]}

plot_spectra(
    [
        x1d_nsclean_skipped[0],
        x1d_nsclean_default[0],
        x1d_nsclean_alternate[0],
        x1d_nsclean_modified[0],
    ],
    wavelength_range=wavelength_range,
    scale_percent=4,
)
plot_spectra(
    [
        x1d_nsclean_skipped[1],
        x1d_nsclean_default[1],
        x1d_nsclean_alternate[1],
        x1d_nsclean_modified[1],
    ],
    wavelength_range=wavelength_range,
    scale_percent=4,
)
../../_images/405ab74a513c19b6dc531e5ba679001c215eae8ba6e92e4e47605a398035123a.png ../../_images/29ea2cf177de9f921c1cacf18a3dbbb84658e5a5baef125cbed7764614dc81e8.png

Note: Cleaning with the alternate mask still removes some of the wavelength-dependent variations but leaves residual background variation in NRS2. Also note the overall continuum level for the spectrum has changed, especially for NRS1. Again, this is because sigma-clipping, unlike the default method, only masks the bright outliers, rather than the entire science region. Consequently, some sky regions are included in the background model that gets subtracted from the data, which can result in changes to the continuum level. In this case, the original algorithm, which blocks the entire science region for each IFU slice (or the hand-modified method to exclude snowballs), is preferable to creating the mask via sigma-clipping.

About the Notebook #

Authors: Melanie Clarke, Kayli Glidic; NIRSpec Instrument Team

Updated On: Feburary 29, 2024.


Top of Page