diff --git a/.github/workflows/ci_deprecate.yml b/.github/workflows/ci_deprecate.yml
new file mode 100644
index 000000000..321117412
--- /dev/null
+++ b/.github/workflows/ci_deprecate.yml
@@ -0,0 +1,66 @@
+name: Deprecate Notebook
+
+on:
+ workflow_dispatch:
+ inputs:
+ notebook_name:
+ description: 'The name of the notebook to deprecate (e.g., example.ipynb)'
+ required: true
+ default: 'example.ipynb'
+
+jobs:
+ deprecate:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Find notebook path
+ id: find_path
+ run: |
+ NOTEBOOK_NAME="${{ github.event.inputs.notebook_name }}"
+ NOTEBOOK_PATH=$(find ./notebooks -name "$NOTEBOOK_NAME" -type f)
+ if [ -z "$NOTEBOOK_PATH" ]; then
+ echo "::error::Notebook '${NOTEBOOK_NAME}' not found in the notebooks directory."
+ exit 1
+ fi
+ echo "notebook_path=$NOTEBOOK_PATH" >> $GITHUB_ENV
+
+ # - name: Check for deprecated tag
+ # id: check_deprecated
+ # run: |
+ # notebook_path="${{ env.notebook_path }}"
+ # if jq '.metadata.deprecated == true' "$notebook_path"; then
+ # echo "::error::Notebook '${{ env.notebook_path }}' is already flagged as deprecated."
+ # exit 0
+ # fi
+
+ - name: Add deprecated tag with timestamp and removal date
+ run: |
+ notebook_path="${{ env.notebook_path }}"
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ removal_date=$(date -u -d "$timestamp + 30 days" +"%Y-%m-%dT%H:%M:%SZ")
+ jq --arg ts "$timestamp" --arg rd "$removal_date" \
+ '.metadata.deprecated = { "status": true, "timestamp": $ts, "removal_date": $rd }' \
+ "$notebook_path" > temp.ipynb && mv temp.ipynb "$notebook_path"
+
+ - name: Add deprecation banner with timestamp and removal date
+ run: |
+ notebook_path="${{ env.notebook_path }}"
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ removal_date=$(date -u -d "$timestamp + 30 days" +"%Y-%m-%d")
+ BANNER_CELL=$(jq -n \
+ --arg text "
⚠️ This notebook is scheduled for deprecation as of $timestamp and is planned for removal by $removal_date. Future use is discouraged.
" \
+ '{"cell_type": "markdown", "metadata": {"deprecation": true}, "source": [$text]}')
+ jq ".cells |= [$BANNER_CELL] + ." "$notebook_path" > temp.ipynb && mv temp.ipynb "$notebook_path"
+
+
+ - name: Commit and push to gh-storage branch
+ run: |
+ git config --global user.name "github-actions"
+ git config --global user.email "github-actions@github.com"
+ git checkout -B gh-storage
+ git add "${{ env.notebook_path }}"
+ git commit -m "Deprecate notebook ${{ env.notebook_path }}"
+ git push origin gh-storage --force
diff --git a/.github/workflows/ci_move_deprecated_notebooks.yml b/.github/workflows/ci_move_deprecated_notebooks.yml
new file mode 100644
index 000000000..6bbff1a00
--- /dev/null
+++ b/.github/workflows/ci_move_deprecated_notebooks.yml
@@ -0,0 +1,82 @@
+name: Move Deprecated Notebooks
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 3 * * *' # Runs daily at 3 AM UTC
+
+jobs:
+ check_and_move:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ with:
+ ref: gh-storage # Start from the gh-storage branch
+
+ - name: Set up date variables
+ id: date_setup
+ run: |
+ CURRENT_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ echo "current_date=$CURRENT_DATE" >> $GITHUB_ENV
+ - name: Find all deprecated notebooks
+ id: find_deprecated
+ run: |
+ # Find all notebooks with the deprecation flag set
+ DEPRECATED_NOTEBOOKS=$(find ./notebooks -name "*.ipynb" -exec jq -r 'select(.metadata.deprecated.status == true) | input_filename' {} \;)
+ if [ -z "$DEPRECATED_NOTEBOOKS" ]; then
+ echo "No deprecated notebooks found."
+ exit 0
+ fi
+ echo "deprecated_notebooks=$DEPRECATED_NOTEBOOKS" >> $GITHUB_ENV
+ - name: Process deprecated notebooks
+ run: |
+ current_date="${{ env.current_date }}"
+ deprecated_notebooks="${{ env.deprecated_notebooks }}"
+
+ for notebook_path in $deprecated_notebooks; do
+ # Extract the removal date from the notebook metadata
+ removal_date=$(jq -r '.metadata.deprecated.removal_date' "$notebook_path")
+ # Check if the current date is past the removal date
+ if [[ "$current_date" > "$removal_date" ]]; then
+ echo "Notebook $notebook_path is past the deprecation date ($removal_date). Moving to 'deprecated' branch."
+ # Determine the notebook's directory
+ notebook_dir=$(dirname "$notebook_path")
+ else
+ echo "Notebook $notebook_path is not past the deprecation date ($removal_date)."
+ fi
+ done
+ # Checkout deprecated branch
+ git fetch origin deprecated
+ git checkout deprecated || git checkout -b deprecated
+
+ # Move the entire folder to the deprecated branch
+ git mv "$notebook_dir" .
+
+ # Commit changes on deprecated branch
+ git add .
+ git commit -m "Moved deprecated notebook $notebook_path to deprecated branch"
+
+ # Push changes to the deprecated branch
+ git push origin deprecated
+
+ # Checkout main branch
+ git checkout main
+
+ # Remove the folder from main
+ git rm -r "$notebook_dir"
+
+ # Commit changes on main branch
+ git add .
+ git commit -m "Removed deprecated notebook $notebook_path from main branch"
+
+ # Push changes to the main branch
+ git push origin main
+
+ # Return to gh-storage branch
+ git checkout gh-storage
+ else
+ echo "Notebook $notebook_path is not past the deprecation date ($removal_date)."
+ fi
+ done
diff --git a/.github/workflows/weekly_html_accessibility_check.yml b/.github/workflows/weekly_html_accessibility_check.yml
index 096f71aa1..4805311e4 100644
--- a/.github/workflows/weekly_html_accessibility_check.yml
+++ b/.github/workflows/weekly_html_accessibility_check.yml
@@ -3,11 +3,26 @@ on:
schedule:
- cron: '0 4 * * 0' # 0400 UTC every Sunday
workflow_dispatch:
+ inputs:
+ total_error_limit:
+ required: false
+ description: 'The maximum total allowed number of HTML accessibility errors. To skip this testing requirement, enter the value "-1". If not explicitly specified, the default value is "0".'
+ default: 0
+ type: string
+ total_warning_limit:
+ required: false
+ description: 'The maximum total allowed number of HTML accessibility warnings. To skip this testing requirement, enter the value "-1". If not explicitly specified, the default value is "0".'
+ default: 0
+ type: string
jobs:
Scheduled:
- uses: spacetelescope/notebook-ci-actions/.github/workflows/html_accessibility_check.yml@main
+ uses: spacetelescope/notebook-ci-actions/.github/workflows/html_accessibility_check.yml@v3
with:
target_url: https://spacetelescope.github.io/${{ github.event.repository.name }}/
+ python-version: ${{ vars.PYTHON_VERSION }}
+ total_error_limit: ${{ inputs.total_error_limit || 0 }}
+ total_warning_limit: ${{ inputs.total_warning_limit || 0 }}
+
secrets:
A11YWATCH_TOKEN: ${{ secrets.A11YWATCH_TOKEN }}
diff --git a/.gitignore b/.gitignore
index c3d895740..8ed89dcd0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,15 @@ __pycache__/
# C extensions
*.so
+
+
+# FITS files
+*.fits
+
+
+
+
+
# Distribution / packaging
.Python
build/
diff --git a/notebooks/MIRI/MIRI_LRS_spectral_extraction/miri_lrs_advanced_extraction_part1.ipynb b/notebooks/MIRI/MIRI_LRS_spectral_extraction/miri_lrs_advanced_extraction_part1.ipynb
index ad83f47bf..cd52752c4 100644
--- a/notebooks/MIRI/MIRI_LRS_spectral_extraction/miri_lrs_advanced_extraction_part1.ipynb
+++ b/notebooks/MIRI/MIRI_LRS_spectral_extraction/miri_lrs_advanced_extraction_part1.ipynb
@@ -15,6 +15,7 @@
"**Cross-intrument:** NIRSpec, MIRI.
\n",
"**Documentation:** This notebook is part of a STScI's larger [post-pipeline Data Analysis Tools Ecosystem](https://jwst-docs.stsci.edu/jwst-post-pipeline-data-analysis) and can be [downloaded](https://github.com/spacetelescope/dat_pyinthesky/tree/main/jdat_notebooks/MRS_Mstar_analysis) directly from the [JDAT Notebook Github directory](https://github.com/spacetelescope/jdat_notebooks).
\n",
"\n",
+ "\n",
"### Introduction: Spectral extraction in the JWST calibration pipeline\n",
"\n",
"The JWST calibration pipeline performs spectrac extraction for all spectroscopic data using basic default assumptions that are tuned to produce accurately calibrated spectra for the majority of science cases. This default method is a simple fixed-width boxcar extraction, where the spectrum is summed over a number of pixels along the cross-dispersion axis, over the valid wavelength range. An aperture correction is applied at each pixel along the spectrum to account for flux lost from the finite-width aperture. \n",
@@ -144,7 +145,7 @@
"if not os.path.exists(\"data/\"):\n",
" print(\"Unpacking Data\")\n",
" with tarfile.open('./data.tar.gz', \"r:gz\") as tar:\n",
- " tar.extractall()"
+ " tar.extractall(filter='data')"
]
},
{
diff --git a/notebooks/MIRI/MIRI_LRS_spectral_extraction/requirements.txt b/notebooks/MIRI/MIRI_LRS_spectral_extraction/requirements.txt
index 0c89bc8d8..2ac92d612 100644
--- a/notebooks/MIRI/MIRI_LRS_spectral_extraction/requirements.txt
+++ b/notebooks/MIRI/MIRI_LRS_spectral_extraction/requirements.txt
@@ -1,5 +1,4 @@
jdaviz >= 3.6.0
astropy >= 5.3.1
jwst >= 1.11.3
-specreduce >= 1.3.0
-
+specreduce >= 1.3.0
\ No newline at end of file
diff --git a/notebooks/MIRI/MRS_Mstar_analysis/JWST_Mstar_dataAnalysis_analysis.ipynb b/notebooks/MIRI/MRS_Mstar_analysis/JWST_Mstar_dataAnalysis_analysis.ipynb
index 09594f6ce..866a536ed 100644
--- a/notebooks/MIRI/MRS_Mstar_analysis/JWST_Mstar_dataAnalysis_analysis.ipynb
+++ b/notebooks/MIRI/MRS_Mstar_analysis/JWST_Mstar_dataAnalysis_analysis.ipynb
@@ -17,7 +17,7 @@
"source": [
"**Use case:** Extract spatial-spectral features from IFU cube and measure their attributes.
\n",
"**Data:** Simulated [MIRI MRS](https://jwst-docs.stsci.edu/mid-infrared-instrument/miri-observing-modes/miri-medium-resolution-spectroscopy) spectrum of AGB star.
\n",
- "**Tools:** specutils, jwst, photutils, astropy, scipy.
\n",
+ "**Tools:** specutils, astropy, scipy.
\n",
"**Cross-intrument:** NIRSpec, MIRI.
\n",
"**Documentation:** This notebook is part of a STScI's larger [post-pipeline Data Analysis Tools Ecosystem](https://jwst-docs.stsci.edu/jwst-post-pipeline-data-analysis) and can be [downloaded](https://github.com/spacetelescope/dat_pyinthesky/tree/main/jdat_notebooks/MRS_Mstar_analysis) directly from the [JDAT Notebook Github directory](https://github.com/spacetelescope/jdat_notebooks).
\n",
"**Source of Simulations:** [MIRISim](https://www.stsci.edu/jwst/science-planning/proposal-planning-toolbox/mirisim)
\n",
@@ -94,7 +94,7 @@
},
"outputs": [],
"source": [
- "# Import astropy packages \n",
+ "# Import astropy packages\n",
"from astropy import units as u\n",
"from astropy.io import ascii\n",
"from astropy.nddata import StdDevUncertainty\n",
@@ -110,7 +110,7 @@
"from jdaviz import Cubeviz\n",
"\n",
"# Display the video\n",
- "from IPython.display import HTML, YouTubeVideo"
+ "from IPython.display import YouTubeVideo"
]
},
{
@@ -127,7 +127,7 @@
" with open(name, 'wb') as f:\n",
" pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)\n",
"\n",
- " \n",
+ "\n",
"def load_obj(name):\n",
" with open(name, 'rb') as f:\n",
" return pickle.load(f)"
@@ -140,7 +140,7 @@
"outputs": [],
"source": [
"def checkKey(dict, key):\n",
- " \n",
+ "\n",
" if key in dict.keys():\n",
" print(\"Present, \", end=\" \")\n",
" print(\"value =\", dict[key])\n",
@@ -169,25 +169,28 @@
"# Check if Pipeline 3 Reduced data exists and, if not, download it\n",
"import os\n",
"import urllib.request\n",
+ "import tarfile\n",
"\n",
"if os.path.exists(\"combine_dithers_all_exposures_ch1-long_s3d.fits\"):\n",
" print(\"Pipeline 3 Data Exists\")\n",
"else:\n",
" url = 'https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/reduced.tar.gz'\n",
" urllib.request.urlretrieve(url, './reduced.tar.gz')\n",
- " # Unzip Tar Files\n",
"\n",
- " import tarfile\n",
+ " base_extract_to = os.path.abspath(\".\") # Current directory\n",
"\n",
- " # Unzip files if they haven't already been unzipped\n",
- " if os.path.exists(\"reduced/\"):\n",
- " print(\"Pipeline 3 Data Exists\")\n",
- " else:\n",
- " tar = tarfile.open('./reduced.tar.gz', \"r:gz\")\n",
- " tar.extractall()\n",
- " tar.close()\n",
- " \n",
- " # Move Files \n",
+ " # Open and securely extract files from the tar archive\n",
+ " with tarfile.open('./reduced.tar.gz', \"r:gz\") as tar:\n",
+ " for member in tar.getmembers():\n",
+ " # Calculate the absolute path of where the file will be extracted\n",
+ " member_path = os.path.abspath(os.path.join(base_extract_to, member.name))\n",
+ "\n",
+ " # Check if the file path is within the base extraction directory\n",
+ " if member_path.startswith(base_extract_to):\n",
+ " # Extract only safe files, directly to the base directory\n",
+ " tar.extract(member, path=base_extract_to)\n",
+ " else:\n",
+ " print(f\"Skipped {member.name} due to potential security risk\")\n",
" os.system('mv reduced/*fits .')"
]
},
@@ -297,7 +300,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# Load in the spectrum list from above. \n",
+ "# Load in the spectrum list from above.\n",
"specviz.load_data(splist)"
]
},
@@ -352,7 +355,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# Here, we load the data into the Cubeviz app for visual inspection. \n",
+ "# Here, we load the data into the Cubeviz app for visual inspection.\n",
"# In this case, we're just looking at a single channel because, unlike Specviz, Cubeviz can only load a single cube at a time.\n",
"\n",
"ch1short_cubefile = 'combine_dithers_all_exposures_ch1-long_s3d.fits'\n",
@@ -363,26 +366,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Next, you want to define a pixel region subset that is specific to the AGB star. You can do this with the regions utility button shown in the video below and drawing a circular region around the AGB star at approximate pixels x=20, y=30."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Video 3:\n",
- " \n",
- "Here is a video that quickly shows how to select a spatial region in Cubeviz."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Video showing the selection of the star with a circular region of interest\n",
- "HTML('')"
+ "Next, you want to define a pixel region subset that is specific to the AGB star. You can do this with the regions utility button and drawing a circular region around the AGB star at approximate pixels x=20, y=30."
]
},
{
@@ -392,16 +376,16 @@
"outputs": [],
"source": [
"# Now extract spectrum from your spectral viewer\n",
+ "# NEED TO show how to use Spectral Extraction plugin and calculate mean instead of sum spectra\n",
"try:\n",
- " spec_agb = cubeviz.get_data('combine_dithers_all_exposures_ch1-long_s3d[SCI]',\n",
- " spatial_subset='Subset 1', function='mean') # AGB star only\n",
+ " spec_agb = cubeviz.get_data('Spectrum (Subset 1, sum)') # AGB star only\n",
" print(spec_agb)\n",
" spec_agb_exists = True\n",
"except Exception:\n",
" print(\"There are no subsets selected.\")\n",
" spec_agb_exists = False\n",
- " spec_agb = cubeviz.get_data('combine_dithers_all_exposures_ch1-long_s3d[SCI]',\n",
- " function='mean') # Whole field of view"
+ " spec_agb = cubeviz.get_data('Spectrum (sum)') # Whole field of view\n",
+ " print(spec_agb)"
]
},
{
@@ -424,12 +408,13 @@
"metadata": {},
"outputs": [],
"source": [
- "wav = wlall*u.micron # Wavelength: microns\n",
- "fl = fnuall*u.Jy # Fnu: Jy\n",
- "efl = dfnuall*u.Jy # Error flux: Jy\n",
+ "wav = wlall*u.micron # Wavelength: microns\n",
+ "fl = fnuall*u.Jy # Fnu: Jy\n",
+ "efl = dfnuall*u.Jy # Error flux: Jy\n",
"\n",
"# Make a 1D spectrum object\n",
- "spec = Spectrum1D(spectral_axis=wav, flux=fl, uncertainty=StdDevUncertainty(efl))"
+ "spec = Spectrum1D(spectral_axis=wav, flux=fl,\n",
+ " uncertainty=StdDevUncertainty(efl))"
]
},
{
@@ -439,9 +424,9 @@
"outputs": [],
"source": [
"# Apply a 5 pixel boxcar smoothing to the spectrum\n",
- "spec_bsmooth = box_smooth(spec, width=5) \n",
+ "spec_bsmooth = box_smooth(spec, width=5)\n",
"\n",
- "# Plot the spectrum & smoothed spectrum to inspect features \n",
+ "# Plot the spectrum & smoothed spectrum to inspect features\n",
"plt.figure(figsize=(8, 4))\n",
"plt.plot(spec.spectral_axis, spec.flux, label='Source')\n",
"plt.plot(spec.spectral_axis, spec_bsmooth.flux, label='Smoothed')\n",
@@ -450,11 +435,14 @@
"plt.ylim(-0.05, 0.15)\n",
"\n",
"# Overplot the original input spectrum for comparison\n",
- "origspecfile = fn = download_file('https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/63702662.txt', cache=True)\n",
+ "origspecfile = fn = download_file(\n",
+ " 'https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/63702662.txt', cache=True)\n",
"origdata = ascii.read(origspecfile)\n",
"wlorig = origdata['col1']\n",
- "fnujyorig = origdata['col2']*0.001 # comes in as mJy, change to Jy to compare with pipeline output\n",
- "plt.plot(wlorig, fnujyorig, '.', color='grey', markersize=1, label='Original Input')\n",
+ "# comes in as mJy, change to Jy to compare with pipeline output\n",
+ "fnujyorig = origdata['col2']*0.001\n",
+ "plt.plot(wlorig, fnujyorig, '.', color='grey',\n",
+ " markersize=1, label='Original Input')\n",
"\n",
"plt.legend(frameon=False, fontsize='medium')\n",
"plt.tight_layout()\n",
@@ -482,26 +470,13 @@
"specviz.show()"
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### Developer Note: Cannot currently open a spectrum1d output from cubeviz in specviz. https://jira.stsci.edu/browse/JDAT-1791\n",
- "\n",
- "#specviz.load_data(spec_agb)"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "# For now, you must create your own spectrum1d object from your extracted cubeviz spectrum. \n",
- "flux = spec_agb.flux\n",
- "wavelength = spec_agb.spectral_axis\n",
- "spec1d = Spectrum1D(spectral_axis=wavelength, flux=flux)\n",
- "specviz.load_data(spec1d)"
+ "specviz.load_data(spec_agb)"
]
},
{
@@ -512,23 +487,13 @@
"specviz.load_data(spec)"
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Video 4:\n",
- " \n",
- "Here is a video that quickly shows how to smooth your spectrum in Specviz."
- ]
- },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "# Video showing how to smooth a spectrum in Specviz\n",
- "HTML('')"
+ "# Make new video to show how to smooth spectrum in Specviz"
]
},
{
@@ -554,23 +519,13 @@
"For now switching to a blackbody."
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Video 5:\n",
- " \n",
- "Here is a video that shows how to fit a blackbody model to the spectrum"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "# Video showing how to fit a blackbody \n",
- "HTML('')"
+ "# Make new video to show how to fit a blackbody model to the spectrum"
]
},
{
@@ -580,14 +535,15 @@
"outputs": [],
"source": [
"spectra = specviz.get_spectra()\n",
- " \n",
+ "\n",
"a = checkKey(spectra, \"BB1\")\n",
"if a is True:\n",
" # Extract Blackbody fit from Specviz\n",
" blackbody = spectra[\"BB1\"]\n",
"else:\n",
" print(\"No Blackbody\")\n",
- " fn = download_file('https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/blackbody.fits', cache=True)\n",
+ " fn = download_file(\n",
+ " 'https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/blackbody.fits', cache=True)\n",
" blackbody = Spectrum1D.read(fn)"
]
},
@@ -667,7 +623,8 @@
"\n",
"# Now subtract the BB and plot the underlying dust continuum\n",
"plt.figure(figsize=(8, 4))\n",
- "plt.plot(spec.spectral_axis, spec.flux.value - ybest.value, color='purple', label='Dust spectra')\n",
+ "plt.plot(spec.spectral_axis, spec.flux.value -\n",
+ " ybest.value, color='purple', label='Dust spectra')\n",
"plt.axhline(0, color='r', linestyle='dashdot', alpha=0.5)\n",
"plt.xlabel('Wavelength (microns)')\n",
"plt.ylabel(\"Flux ({:latex})\".format(spec.flux.unit))\n",
@@ -745,23 +702,13 @@
"specviz.load_data(bbsub_spectra)"
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Video 6:\n",
- " \n",
- "Here is a video that shows how to fit a polynomial to two separate spectral regions within a single subset to remove more underlying continuum"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "# Video showing how to fit a polynomial to two separate spectral regions within a single subset\n",
- "HTML('')"
+ "# Make new video to show how to fit a polynomial to two separate spectral regions within a single subset"
]
},
{
@@ -771,14 +718,15 @@
"outputs": [],
"source": [
"spectra = specviz.get_spectra()\n",
- " \n",
+ "\n",
"a = checkKey(spectra, \"PolyFit\")\n",
"if a is True:\n",
" # Extract polynomial fit from Specviz\n",
" poly = spectra[\"PolyFit\"]\n",
"else:\n",
" print(\"No Polyfit\")\n",
- " fn = download_file('https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/poly.fits', cache=True)\n",
+ " fn = download_file(\n",
+ " 'https://data.science.stsci.edu/redirect/JWST/jwst-data_analysis_tools/MRS_Mstar_analysis/poly.fits', cache=True)\n",
" poly = Spectrum1D.read(fn)"
]
},
@@ -828,8 +776,10 @@
"# -----------------------------------------------------------------\n",
"# Generate a continuum subtracted and continuum normalised spectra\n",
"\n",
- "line_spec_norm = Spectrum1D(spectral_axis=line_spec.spectral_axis, flux=line_spec.flux/line_y_continuum, uncertainty=StdDevUncertainty(np.zeros(len(line_spec.spectral_axis))))\n",
- "line_spec_consub = Spectrum1D(spectral_axis=line_spec.spectral_axis, flux=line_spec.flux - line_y_continuum, uncertainty=StdDevUncertainty(np.zeros(len(line_spec.spectral_axis))))\n",
+ "line_spec_norm = Spectrum1D(spectral_axis=line_spec.spectral_axis, flux=line_spec.flux /\n",
+ " line_y_continuum, uncertainty=StdDevUncertainty(np.zeros(len(line_spec.spectral_axis))))\n",
+ "line_spec_consub = Spectrum1D(spectral_axis=line_spec.spectral_axis, flux=line_spec.flux -\n",
+ " line_y_continuum, uncertainty=StdDevUncertainty(np.zeros(len(line_spec.spectral_axis))))\n",
"\n",
"# -----------------------------------------------------------------\n",
"# Plot the dust feature & continuum fit to the region\n",
@@ -886,25 +836,13 @@
"specviz.load_data(line_spec_norm, data_label='Normalized')"
]
},
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Video7:\n",
- " \n",
- "Here is a video that shows how to make line analysis measurements within specviz (i.e., line flux, line centroid, equivalent width)
\n",
- "Note: You want to calculate your equivalent width on the normalized spectrum
\n",
- "Note: You can also hack to convert the line flux value into more conventional units
"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "# Video showing how to measure lines within specviz\n",
- "HTML('')"
+ "# Make new video to show how to measure lines within specviz"
]
},
{
@@ -915,8 +853,10 @@
"source": [
"# Alternative method to analyze the 10um line within the notebook. Calculate the Line flux; Line Centroid; Equivalent width\n",
"\n",
- "line_centroid = centroid(line_spec_consub, SpectralRegion(sw_line*u.um, lw_line*u.um))\n",
- "line_flux_val = line_flux(line_spec_consub, SpectralRegion(sw_line*u.um, lw_line*u.um))\n",
+ "line_centroid = centroid(\n",
+ " line_spec_consub, SpectralRegion(sw_line*u.um, lw_line*u.um))\n",
+ "line_flux_val = line_flux(\n",
+ " line_spec_consub, SpectralRegion(sw_line*u.um, lw_line*u.um))\n",
"\n",
"equivalent_width_val = equivalent_width(line_spec_norm)\n",
"\n",
@@ -953,7 +893,7 @@
"plt.figure(figsize=(10, 6))\n",
"plt.plot(optdepth_spec.spectral_axis, optdepth_spec.flux)\n",
"plt.xlabel(\"Wavelength ({:latex})\".format(spec.spectral_axis.unit))\n",
- "plt.ylabel('Tau') \n",
+ "plt.ylabel('Tau')\n",
"plt.tight_layout()\n",
"plt.show()\n",
"plt.close()"
@@ -996,7 +936,8 @@
"**Author:** Olivia Jones, Project Scientist, UK ATC.
\n",
"**Updated On:** 2020-08-11
\n",
"**Updated On:** 2021-09-06 by B. Sargent, STScI Scientist, Space Telescope Science Institute (added MRS Simulated Data)
\n",
- "**Updated On:** 2021-12-12 by O. Fox, STScI Scientist (added blackbody and polynomial fitting within the notebook)
"
+ "**Updated On:** 2021-12-12 by O. Fox, STScI Scientist (added blackbody and polynomial fitting within the notebook)
\n",
+ "**Updated On:** 2024-10-29 by C. Pacifici, STScI Data Scientist, adapt to Jdaviz 4.0 (still need to update videos)
"
]
},
{
@@ -1030,7 +971,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.13"
+ "version": "3.12.7"
}
},
"nbformat": 4,
diff --git a/notebooks/MIRI/psf_photometry/miri_1028.ipynb b/notebooks/MIRI/psf_photometry/miri_1028.ipynb
new file mode 100644
index 000000000..35d38952a
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/miri_1028.ipynb
@@ -0,0 +1,1893 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b58d6954",
+ "metadata": {},
+ "source": [
+ "# MIRI PSF Photometry With Space_Phot\n",
+ "\n",
+ "**Author**: Ori Fox
\n",
+ "\n",
+ "**Submitted**: August, 2023
\n",
+ "**Updated**: November, 2023
\n",
+ "\n",
+ "**Use case**: PSF Photometry on Level3 data using dedicated package Space_Phot (https://github.com/jpierel14/space_phot). Space_phot is built on Astropy's Photutils package. Unlike photutils, space_phot can be used on Level3 data. This is because has built in functionality that can generate a resampled Level3 PSF at a given detector position. Such use cases are particularly useful for faint, point source targets or deep upper limits where observers need to take advantage of the combined image stack. For a large number of bright sources, users may find space_phot to be too slow and should consider other packages, such as DOLPHOT and/or Photutils. **NOTE:** A companion notebook exists that illustrates how to use Photutils for the same Level2 data set.
\n",
+ "**Important Note**: When not to use. Due to the sensitivity of the space_phot parameters, this tool is not meant to be used for a large sample of stars (i.e., Section 5 below). If a user would like to use space_phot on more than one source, they should carefully construct a table of parameters that are carefully refined for each source.\n",
+ "**Data**: MIRI Data PID 1028 (Calibration Program; Single Star Visit 006 A5V dwarf 2MASSJ17430448+6655015) and MIRI Data PID 1171 (LMC; Multiple Stars).
\n",
+ "**Tools**: photutils, space_phot drizzlepac, jupyter
\n",
+ "**Cross-Instrument**: NIRCam, MIRI.
\n",
+ "**Documentation**: This notebook is part of a STScI's larger post-pipeline Data Analysis Tools Ecosystem and can be downloaded directly from the JDAT Notebook Github directory.
\n",
+ "**Pipeline Version**: JWST Pipeline
\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "88c61bcf-1c4d-407a-b80c-aa13a01fd746",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Table of contents\n",
+ "1. [Introduction](#intro)
\n",
+ " 1.1 [Setup](#webbpsf)
\n",
+ " 1.2 [Python imports](#py_imports)
\n",
+ "2. [Download Data](#data)
\n",
+ "3. [Bright, Single Object](#bso)
\n",
+ " 3.1 [Multiple, Level2 Files](#bso2)
\n",
+ " 3.2 [Single, Level3 Mosaicked File](#bso3)
\n",
+ "4. [Faint/Upper Limit, Single Object](#fso)
\n",
+ " 4.1 [Multiple, Level2 Files](#fso2)
\n",
+ " 4.2 [Single, Level3 Mosaicked File](#fso3)
\n",
+ "5. [Stellar Field (LMC)](#lmv)
\n",
+ " 5.1 [Multiple, Level2 Files](#lmc2)
\n",
+ " 5.2 [Single, Level3 Mosaicked File](#lmc3)
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f572688",
+ "metadata": {},
+ "source": [
+ "1.-Introduction \n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "95891849",
+ "metadata": {},
+ "source": [
+ "GOALS:
\n",
+ "\n",
+ "PSF Photometry can be obtained using:
\n",
+ "\n",
+ "* grid of PSF models from WebbPSF
\n",
+ "* single effective PSF (ePSF) NOT YET AVAILABLE
\n",
+ "* grid of effective PSF NOT YET AVAILABLE
\n",
+ "\n",
+ "The notebook shows:
\n",
+ "\n",
+ "* how to obtain the PSF model from WebbPSF (or build an ePSF)
\n",
+ "* how to perform PSF photometry on the image
\n",
+ "\n",
+ "**Data**:
\n",
+ "\n",
+ "MIRI Data PID 1028 (Calibration Program), F770W
\n",
+ "MIRI Data PID 1171 (LMC), F560W/F770W"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b762602",
+ "metadata": {},
+ "source": [
+ "### 1.1-Setup WebbPSF and Synphot Directories ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d7df5c79-da6b-4d50-bca3-23256e9afc80",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import space_phot\n",
+ "from importlib.metadata import version\n",
+ "print('space-phot version : ', version('space_phot'))\n",
+ "print('jwst version : ', version('jwst'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c50eace",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import glob\n",
+ "import shutil\n",
+ "import requests\n",
+ "import tarfile\n",
+ "from urllib.parse import urlparse\n",
+ "\n",
+ "# Set environmental variables\n",
+ "os.environ[\"WEBBPSF_PATH\"] = \"./webbpsf-data/webbpsf-data\"\n",
+ "os.environ[\"PYSYN_CDBS\"] = \"./grp/redcat/trds/\"\n",
+ "\n",
+ "# WEBBPSF Data\n",
+ "boxlink = 'https://stsci.box.com/shared/static/qxpiaxsjwo15ml6m4pkhtk36c9jgj70k.gz' \n",
+ "boxfile = './webbpsf-data/webbpsf-data-1.0.0.tar.gz'\n",
+ "synphot_url = 'http://ssb.stsci.edu/trds/tarfiles/synphot5.tar.gz'\n",
+ "synphot_file = './synphot5.tar.gz'\n",
+ "\n",
+ "webbpsf_folder = './webbpsf-data'\n",
+ "synphot_folder = './grp'\n",
+ "\n",
+ "\n",
+ "def download_file(url, dest_path, timeout=60):\n",
+ " parsed_url = urlparse(url)\n",
+ " if parsed_url.scheme not in [\"http\", \"https\"]:\n",
+ " raise ValueError(f\"Unsupported URL scheme: {parsed_url.scheme}\")\n",
+ "\n",
+ " response = requests.get(url, stream=True, timeout=timeout)\n",
+ " response.raise_for_status()\n",
+ " with open(dest_path, \"wb\") as f:\n",
+ " for chunk in response.iter_content(chunk_size=8192):\n",
+ " f.write(chunk)\n",
+ "\n",
+ "\n",
+ "# Gather webbpsf files\n",
+ "psfExist = os.path.exists(webbpsf_folder)\n",
+ "if not psfExist:\n",
+ " os.makedirs(webbpsf_folder)\n",
+ " download_file(boxlink, boxfile)\n",
+ " gzf = tarfile.open(boxfile)\n",
+ " gzf.extractall(webbpsf_folder, filter='data')\n",
+ "\n",
+ "# Gather synphot files\n",
+ "synExist = os.path.exists(synphot_folder)\n",
+ "if not synExist:\n",
+ " os.makedirs(synphot_folder)\n",
+ " download_file(synphot_url, synphot_file)\n",
+ " gzf = tarfile.open(synphot_file)\n",
+ " gzf.extractall('./', filter='data')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5e534877-5c31-4020-9263-4f234f19e1cd",
+ "metadata": {},
+ "source": [
+ "### 1.2-Python Imports ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "93e91ff7-ea71-4507-b38d-c2067cae3a8f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from astropy.io import fits\n",
+ "from astropy.nddata import extract_array\n",
+ "from astropy.coordinates import SkyCoord\n",
+ "from astropy import wcs\n",
+ "from astropy.table import QTable\n",
+ "from astropy.wcs.utils import skycoord_to_pixel\n",
+ "from astropy import units as u\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "from astroquery.mast import Observations\n",
+ "from astropy.visualization import simple_norm\n",
+ "import time\n",
+ "import math\n",
+ "import pandas as pd\n",
+ "%matplotlib inline\n",
+ "\n",
+ "# JWST models\n",
+ "from jwst.datamodels import ImageModel\n",
+ "\n",
+ "# Background and PSF Functions\n",
+ "from photutils.background import MMMBackground, MADStdBackgroundRMS\n",
+ "from photutils.detection import DAOStarFinder"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "68f0b2d7-45a1-4511-858e-51425a50de00",
+ "metadata": {},
+ "source": [
+ "2.-Download Data\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6629878-a5d4-4e29-a56e-0f12271016a5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ "# with proposal ID 1028 and a specific filter 'F770W'\n",
+ "obs = Observations.query_criteria(proposal_id=1028, filters=['F770W'])\n",
+ "\n",
+ "# Get a list of products associated with the located observation\n",
+ "plist = Observations.get_product_list(obs)\n",
+ "\n",
+ "# Filter the product list to include only specific product subgroups: 'RATE', 'CAL', 'I2D', and 'ASN'\n",
+ "fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ "\n",
+ "# Download the selected products from the MAST database\n",
+ "Observations.download_products(fplist)\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01028/'\n",
+ "\n",
+ "# Create the destination directory if it doesn't exist\n",
+ "if not os.path.exists(destination_dir):\n",
+ " os.makedirs(destination_dir)\n",
+ "\n",
+ "# Use glob to find all files matching the pattern 'mastDownload/JWST/j*/jw01537*cal.fits'\n",
+ "files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01028*'))\n",
+ "\n",
+ "# Copy the matching files to the destination directory\n",
+ "for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b649b6f4-aa18-4760-a7d0-979b4e3caec2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ "# with proposal ID 1171 and a specific filters 'F560W' and 'F770W'\n",
+ "obs = Observations.query_criteria(proposal_id=1171, filters=['F560W', 'F770W'])\n",
+ "\n",
+ "# Get a list of products associated with the located observation\n",
+ "plist = Observations.get_product_list(obs)\n",
+ "\n",
+ "# Filter the product list to include only specific product subgroups: 'RATE', 'CAL', 'I2D', and 'ASN'\n",
+ "fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ "fplist\n",
+ "\n",
+ "# Download the selected products from the MAST database (UNCOMMENT TO DOWNLOAD)\n",
+ "Observations.download_products(fplist)\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01171/'\n",
+ "\n",
+ "# Create the destination directory if it doesn't exist\n",
+ "if not os.path.exists(destination_dir):\n",
+ " os.makedirs(destination_dir)\n",
+ "\n",
+ "# Use glob to find all files matching the pattern 'mastDownload/JWST/j*/jw01537*cal.fits'\n",
+ "files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01171*'))\n",
+ "\n",
+ "# Copy the matching files to the destination directory\n",
+ "for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5611799d",
+ "metadata": {},
+ "source": [
+ "3.-Bright, Single Object\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6d052f0e-dcb4-4c2a-bc11-4b467dad07c2",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to perform PSF photometry on a single, bright object. While aperture photometry is feasible in isolated cases, the user may find PSF photometry preferable in crowded fields or complicated backgrounds."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55c52f95",
+ "metadata": {},
+ "source": [
+ "### 3.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "058a14ad-be89-4d7e-934e-1e0a909319c8",
+ "metadata": {},
+ "source": [
+ "Generally, PSF photometry for data from a space telescope is most accurately performed on pre-mosaiced data. In the case of HST, that corresponds to FLT files rather than DRZ. And in the case of JWST, this corresponds to Level2 files rather than Level3. The reason is that a mosaiced PSF changes the inherent PSF as a function of position on the detector so that there is no adequate model (theoretical or empirical) to use.
\n",
+ "\n",
+ "In this example, we aim to fit a source simultaneously across multiple Level 2 images. A more basic approach would be to fit each Level 2 file individually and then average together the measured fluxes. However, this approach more easily corrects for bad pixels or cosmic rays that are only in one image and allows for a more accurate photometric solution by reducing the number of free parameters per source.
\n",
+ "\n",
+ "Useful references:
\n",
+ "HST Documentation on PSF Photometry: https://www.stsci.edu/hst/instrumentation/wfc3/data-analysis/psf
\n",
+ "WFPC2 Stellar Photometry with HSTPHOT: https://ui.adsabs.harvard.edu/abs/2000PASP..112.1383D/abstract
\n",
+ "Space-Phot Documentation on Level2 Fitting: https://space-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8f44a6cf",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Define Level 3 File\n",
+ "lvl3 = ['./mast/01028/jw01028-o006_t001_miri_f770w_i2d.fits']\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d35e67aa",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create Level 2 Data List from ASN files\n",
+ "prefix = \"./mast/01028/\"\n",
+ "asn = glob.glob(prefix+'jw01028-o006_*_image3_00004_asn.json')\n",
+ "\n",
+ "with open(asn[0], \"r\") as fi:\n",
+ " lvl2 = []\n",
+ " for ln in fi:\n",
+ " #print(ln)\n",
+ " if ln.startswith(' \"expname\":'):\n",
+ " x = ln[2:].split(':')\n",
+ " y = x[1].split('\"')\n",
+ " lvl2.append(prefix+y[1])\n",
+ "\n",
+ "print(lvl2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab0ac799-a832-40af-9cd4-b5b3934cfee4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (Before DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b89fed70-2f65-4fa6-9242-87f98372e887",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (Before DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ab947da-be0a-4cde-88eb-2d947adf2b81",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "for file in lvl2:\n",
+ " ref_fits = ImageModel(file)\n",
+ " data = ref_fits.data\n",
+ " dq = ref_fits.dq\n",
+ " data[dq >= 10] = np.nan\n",
+ " ref_fits.data = data\n",
+ " ref_fits.save(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21d3d16a-84a8-44af-84b2-c7eb0dbf230e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "for file in lvl2:\n",
+ " hdul = fits.open(file, mode='update')\n",
+ " data = fits.open(file)['SCI', 1].data\n",
+ " dq = fits.open(file)['DQ', 1].data\n",
+ " data[dq >= 10] = np.nan\n",
+ " hdul['SCI', 1].data = data\n",
+ " hdul.flush()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "87673dd8-1bc6-4663-bebc-8e2434974ceb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (After DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4858f4d9-dbc9-40f1-a00c-80339cc69fae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source. In this case, our source is from MIRI Program ID #1028, a Calibration Program.\n",
+ "# We are using Visit 006, which targets the A5V dwarf 2MASSJ17430448+6655015\n",
+ "# Reference Link: http://simbad.cds.unistra.fr/simbad/sim-basic?Ident=2MASSJ17430448%2B6655015&submit=SIMBAD+search\n",
+ "\n",
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "#ref_wcs = WCS(ref_fits[0].header)\n",
+ "\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.3, max_cut=15)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "30f99a12-b3c9-4cf7-a8cc-a98f848dc6d1",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (After DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31bcb979-870c-4a98-8d89-eb825d05e45b",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source\n",
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "plt.imshow(ref_cutout, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d67d57b9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get the PSF from WebbPSF using defaults.\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "psfs = space_phot.get_jwst_psf(jwst_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cee4ba3b-2a71-4685-ae1f-5beb60c7d4ad",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psfs[0].data, (41, 41), (122, 122))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.2)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bf53473e-9671-4f80-9d59-29c4ed8bab09",
+ "metadata": {},
+ "source": [
+ "#### Notes on the PSF Fitting in Space_Phot:
\n",
+ "\n",
+ "https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "As noted above, improved documentation will be coming. For now, here are some important points to consider.\n",
+ "\n",
+ "All fitting is performed with Astropy's Photutils. As with any photometry program, the printed statistical errors are good indicators of your success.\n",
+ "\n",
+ "There are different fitting techniques, but when the fit_flux parameter is set to 'single', the source is fit simultaneously in all Level2 images. There is good reason for this outlined in a paper for PSF fitting in Hubble: https://iopscience.iop.org/article/10.1086/316630/pdf\n",
+ "\n",
+ "As a result, the flux and corresponding error take into account a single overall fit. As part of this, the fitting therefore assumes a constant zero point across all images. While this is not exactly true, it is typically true to within 1\\% and good enough for our purposes. Users can alternatively fit with the fit_flux parameter set to 'multi', which treats each image independently. The final flux must therefore be averaged.\n",
+ "\n",
+ "When you run space_phot, you will see some additional diagnositics displayed in the cell. At the top, the % value printed is the fraction of flux remaining in the residual and can be considered a good indicator of a successful model and subtraction. Next are three columns displaying the original, the model, and the residual, respectively, for each Level2 image. Finally, there are corner plots suggesting the success of the fits (more documentation and explanation of these plots is coming).\n",
+ "\n",
+ "In this case, you will notice a systematic trend in the residuals. The PSF is oversubtracted in the centermost pixel and undersubtracted in the wings. The cause is unknown. It may be due to a poor PSF model. The user should consider generating more complex PSF models, but that is beyond the scope of this notebook. Nonetheless, the residual value is pretty good so the overall statistical error is relatively small."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c9a1b447",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10000, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_flux='single'\n",
+ ")\n",
+ "\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()\n",
+ "\n",
+ "print(jwst_obs.psf_result.phot_cal_table)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "46b39817-d45e-4504-9e0a-281ef405f695",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "jwst_obs.psf_result.phot_cal_table"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31bd70f0",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Print Magnitude from Table\n",
+ "# As noted above, As a result, the flux and corresponding error take into account a single overall fit. \n",
+ "# Therefore, there is no need to average the resulting magnitudes or errors. They should all be the same to within their individual zero-point differences (typically <1%).\n",
+ "mag_lvl2_arr = jwst_obs.psf_result.phot_cal_table['mag']\n",
+ "magerr_lvl2_arr = jwst_obs.psf_result.phot_cal_table['magerr']\n",
+ "\n",
+ "print(mag_lvl2_arr, '\\n', magerr_lvl2_arr)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "af070d39-4c07-4801-b98f-dd9d3be0d7fe",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "jwst_obs_fast = space_phot.observation2(lvl2[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c4785b0-4caf-4fef-848f-4842e91a6eac",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ref_x"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ed225d52-9d2c-4641-a37f-922e8fa696fc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "centers = [ref_x, ref_y]\n",
+ "jwst_obs_fast.fast_psf(psfs[0], centers)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a57f9275",
+ "metadata": {},
+ "source": [
+ "### 3.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "62a15eb8-2a9d-4f5a-895a-b18dc33b24e4",
+ "metadata": {},
+ "source": [
+ "Despite the above discussion on performing PSF photometry on the pre-mosaiced data products, space_phot has the functionality to create a mosaiced Level3 PSF at a given single position on the detector based on the Level2 images. The advantage to this is the ability to perform PSF photometry on the deep, stacked data in cases where faint sources are expected to have prohibitively low signal-to-noise in Level2 data. The disadvantage is the amount of time required to make mosaiced Level3 PSF, so that this method is most useful when dealing with a small number of low signal-to-noise sources.
\n",
+ "\n",
+ "Useful references:
\n",
+ "Space-Phot Documentation on Level3 Fitting: https://space-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#level-3-psf
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7bd91d15",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level3 data file the same as above.\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31eb83ad-b752-4a51-ac9a-6e44f59495f9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0cd06c0a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.5, max_cut=30)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "375143a8-e3a5-4a44-b8bd-ba91875d1aa4",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7bea076d",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF\n",
+ "\n",
+ "# The function get_jwst_psf is a space_phot wrapper for the WebbPSF calc_psf function and uses a lot of the same keywords.\n",
+ "# There are more advanced methods for generating your WebbPSF, but those are beyond the scope of this notebook.\n",
+ "# The defaults used by get_jwst_psf in this notebook are:\n",
+ "# oversample=4\n",
+ "# normalize='last'\n",
+ "# Non-distorted PSF\n",
+ "# Useful reference: https://webbpsf.readthedocs.io/en/latest/api/webbpsf.JWInstrument.html#webbpsf.JWInstrument.calc_psf\n",
+ "\n",
+ "jwst3_obs = space_phot.observation3(lvl3[0])\n",
+ "psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location) # ,num_psfs=4)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d126d410-7a21-45e9-b0ad-8b9a8308a174",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psf3.data, (161, 161), (200, 200))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.01)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model (Mosaiced)')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "990505bf-184e-456f-9a7e-14df8316414c",
+ "metadata": {},
+ "source": [
+ "#### Notes on the PSF Fitting in Space_Phot:
\n",
+ "\n",
+ "https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "As noted above, improved documentation will be coming. For now, here are some important points to consider.\n",
+ "See detailed notes in Section 3.1 above about the fitting process and diagnostics\n",
+ "\n",
+ "In addition, consider here that jwst3_obs is generating a Level3 PSF by using the JWST pipeline to resample and combine multiple Level2 PSFs. The Level2 PSFs are generated at the precise location of the source in each Level2 file to account for detector level effects. The resampling uses default resampling paramters. However, users should be aware that if they performed customized resampling for their Level2 data products, they should use similar resampling steps for their PSF below."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27525a0a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "# See detailed notes in Section 3.1 above about the fitting process and diagnostics\n",
+ "jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10000, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_flux=True\n",
+ ")\n",
+ "\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "dc1f930a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "mag_lvl3psf = jwst3_obs.psf_result.phot_cal_table['mag'][0]\n",
+ "magerr_lvl3psf = jwst3_obs.psf_result.phot_cal_table['magerr'][0]\n",
+ "\n",
+ "print(round(mag_lvl2_arr[0], 4), round(magerr_lvl2_arr[0], 4))\n",
+ "print(round(mag_lvl3psf, 5), round(magerr_lvl3psf, 5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4920c8b1-da54-434c-a8b9-4427c3157a62",
+ "metadata": {},
+ "source": [
+ "## Good agreement between Level2 and level3 results!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b5f0ad5-b59e-4eff-8687-f6a2199d8bd9",
+ "metadata": {},
+ "source": [
+ "4.-Faint/Upper Limit, Single Object\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1dc60da1-0f4d-4e5e-a109-f7352dfd0fdc",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to calculate an upper limit using PSF photometry a blank part of the sky. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8af6f83a-5925-44d5-be0f-facd2316d1ca",
+ "metadata": {},
+ "source": [
+ "### 4.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29ac6f64",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files\n",
+ "lvl3 = ['mast/01028/jw01028-o006_t001_miri_f770w_i2d.fits']\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3ebedb35",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create Level 2 Data List from ASN files\n",
+ "prefix = \"./mast/01028/\"\n",
+ "asn = glob.glob(prefix+'jw01028-o006_*_image3_00004_asn.json')\n",
+ "\n",
+ "with open(asn[0], \"r\") as fi:\n",
+ " lvl2 = []\n",
+ " for ln in fi:\n",
+ " #print(ln)\n",
+ " if ln.startswith(' \"expname\":'):\n",
+ " x = ln[2:].split(':')\n",
+ " y = x[1].split('\"')\n",
+ " lvl2.append(prefix+y[1])\n",
+ " \n",
+ "print(lvl2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "322b5a27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "for file in lvl2:\n",
+ " ref_fits = ImageModel(file)\n",
+ " data = ref_fits.data\n",
+ " dq = ref_fits.dq\n",
+ " data[dq >= 10] = np.nan\n",
+ " ref_fits.data = data\n",
+ " ref_fits.save(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2b5c7fea-3df3-4434-92cc-c7d8afa531dc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (After DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6f4ed7dc-bb03-4dca-9908-2130d96f7c63",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "eac6b4b7-0d02-45f8-9f96-8ea82c589126",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Examine the First Image\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7b2c277e-43d8-4429-9612-2b4440cbdbb9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "plt.imshow(ref_cutout, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "72b8c907",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF\n",
+ "# The function get_jwst_psf is a space_phot wrapper for the WebbPSF calc_psf function and uses a lot of the same keywords.\n",
+ "# There are more advanced methods for generating your WebbPSF, but those are beyond the scope of this notebook.\n",
+ "# The defaults used by get_jwst_psf in this notebook are:\n",
+ "# oversample=4\n",
+ "# normalize='last'\n",
+ "# Non-distorted PSF\n",
+ "# Useful reference: https://webbpsf.readthedocs.io/en/latest/api/webbpsf.JWInstrument.html#webbpsf.JWInstrument.calc_psf\n",
+ "\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "psfs = space_phot.get_jwst_psf(jwst_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "04793a1d-5406-4516-9db8-5845a9f97194",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psf3.data, (161, 161), (200, 200))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.01)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a2615569",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "# https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10, 1000],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=True,\n",
+ " fit_centroid='fixed',\n",
+ " fit_flux='single'\n",
+ ")\n",
+ "\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()\n",
+ "\n",
+ "print(jwst_obs.psf_result.phot_cal_table)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "234c8a45",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# As noted above, As a result, the flux and corresponding error take into account a single overall fit. \n",
+ "# Therefore, there is no need to average the resulting magnitudes or errors. They should all be the same to within their individual zero-point differences (typically <1%).\n",
+ "magupper_lvl2psf = jwst_obs.upper_limit(nsigma=5)\n",
+ "magupper_lvl2psf"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e325db03-6e9b-4f04-be7b-5e80063dd9b8",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "### 4.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2e7ce46d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level3 data file the same as above.\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ffe34f40-6e67-4357-af85-908f92deb889",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "26c66637-1ee7-44b2-92a0-c042ac91e10d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e06609c2-a895-4666-b7c5-d3d3697e1923",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b652b4cf-0d19-49b7-8b09-ecc9c24c6b23",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "plt.imshow(ref_cutout, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006 (level 3)')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "50fbb856",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF\n",
+ "# The function get_jwst_psf is a space_phot wrapper for the WebbPSF calc_psf function and uses a lot of the same keywords.\n",
+ "# There are more advanced methods for generating your WebbPSF, but those are beyond the scope of this notebook.\n",
+ "# The defaults used by get_jwst_psf in this notebook are:\n",
+ "# oversample=4\n",
+ "# normalize='last'\n",
+ "# Non-distorted PSF\n",
+ "# Useful reference: https://webbpsf.readthedocs.io/en/latest/api/webbpsf.JWInstrument.html#webbpsf.JWInstrument.calc_psf\n",
+ "\n",
+ "jwst3_obs = space_phot.observation3(lvl3[0])\n",
+ "psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "37eaaa85-da28-4dd6-a1c5-727b718f1831",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psf3.data, (161, 161), (200, 200))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.01)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fe699262",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-1000, 1000],\n",
+ " # 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_centroid=False,\n",
+ " fit_flux=True\n",
+ ")\n",
+ "\n",
+ "jwst3_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst3_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ecaec8db",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "magupper_lvl3psf = jwst3_obs.upper_limit(nsigma=5)\n",
+ "print(round(magupper_lvl2psf[0], 4))\n",
+ "print(round(magupper_lvl3psf[0], 5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e4996d2-6274-473d-b13c-f3848c27ad78",
+ "metadata": {},
+ "source": [
+ "## Note you can go significantly deeper with the Level3 combined data product"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a969717-bbef-40b9-ac9b-f83dec99dc09",
+ "metadata": {},
+ "source": [
+ "5.-Stellar Field (LMC)\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "da877310-fd47-41d6-afea-7fa725a546af",
+ "metadata": {},
+ "source": [
+ "#### In this case, we are going to do the same steps as in Section 3, but for multiple stars. The purpose is to illustrate the workflow and runtime for using space_phot on a large number of stars. We suggest that space_phot may be less optimal for large numbers of bright stars. Other programs, such as DOLPHOT or Photutils, may be better suited for this use case. The primary advantage to space_phot is on faint, single sources. But it can be extended to a larger number if desired."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32bdafe6-db19-4080-9587-b9785c2f7fa7",
+ "metadata": {},
+ "source": [
+ "### 5.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b618756f",
+ "metadata": {},
+ "source": [
+ "##### Now do the same thing for a larger group of stars and test for speed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "838bd76d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files\n",
+ "lvl3 = [\"./mast/01171/jw01171-o004_t001_miri_f560w_i2d.fits\"]\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "73aba802",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 2 Files\n",
+ "lvl2 = glob.glob('./mast/01171/jw01171004*cal.fits')\n",
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "57f9d790",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Find Stars in Level 3 File\n",
+ "# Get rough estimate of background (There are Better Ways to Do Background Subtraction)\n",
+ "bkgrms = MADStdBackgroundRMS()\n",
+ "mmm_bkg = MMMBackground()\n",
+ "\n",
+ "ref_fits = ImageModel(lvl3[0])\n",
+ "w = ref_fits.get_fits_wcs()\n",
+ "\n",
+ "std = bkgrms(ref_fits.data)\n",
+ "bkg = mmm_bkg(ref_fits.data)\n",
+ "data_bkgsub = ref_fits.data.copy()\n",
+ "data_bkgsub -= bkg \n",
+ "sigma_psf = 1.636 # pixels for F770W\n",
+ "threshold = 5.\n",
+ "\n",
+ "daofind = DAOStarFinder(threshold=threshold * std, fwhm=sigma_psf, exclude_border=True)\n",
+ "found_stars = daofind(data_bkgsub)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e4cee97c",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "found_stars.pprint_all(max_lines=10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7c7d793b",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Filter out only stars you want\n",
+ "plt.figure(figsize=(12, 8))\n",
+ "plt.clf()\n",
+ "\n",
+ "ax1 = plt.subplot(2, 1, 1)\n",
+ "\n",
+ "ax1.set_xlabel('mag')\n",
+ "ax1.set_ylabel('sharpness')\n",
+ "\n",
+ "xlim0 = np.min(found_stars['mag']) - 0.25\n",
+ "xlim1 = np.max(found_stars['mag']) + 0.25\n",
+ "ylim0 = np.min(found_stars['sharpness']) - 0.15\n",
+ "ylim1 = np.max(found_stars['sharpness']) + 0.15\n",
+ "\n",
+ "ax1.set_xlim(xlim0, xlim1)\n",
+ "ax1.set_ylim(ylim0, ylim1)\n",
+ "\n",
+ "ax1.scatter(found_stars['mag'], found_stars['sharpness'], s=10, color='k')\n",
+ "\n",
+ "sh_inf = 0.40\n",
+ "sh_sup = 0.82\n",
+ "#mag_lim = -5.0\n",
+ "lmag_lim = -3.0\n",
+ "umag_lim = -5.0\n",
+ "\n",
+ "ax1.plot([xlim0, xlim1], [sh_sup, sh_sup], color='r', lw=3, ls='--')\n",
+ "ax1.plot([xlim0, xlim1], [sh_inf, sh_inf], color='r', lw=3, ls='--')\n",
+ "ax1.plot([lmag_lim, lmag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "ax1.plot([umag_lim, umag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "\n",
+ "ax2 = plt.subplot(2, 1, 2)\n",
+ "\n",
+ "ax2.set_xlabel('mag')\n",
+ "ax2.set_ylabel('roundness')\n",
+ "\n",
+ "ylim0 = np.min(found_stars['roundness2']) - 0.25\n",
+ "ylim1 = np.max(found_stars['roundness2']) - 0.25\n",
+ "\n",
+ "ax2.set_xlim(xlim0, xlim1)\n",
+ "ax2.set_ylim(ylim0, ylim1)\n",
+ "\n",
+ "round_inf = -0.40\n",
+ "round_sup = 0.40\n",
+ "\n",
+ "ax2.scatter(found_stars['mag'], found_stars['roundness2'], s=10, color='k')\n",
+ "\n",
+ "ax2.plot([xlim0, xlim1], [round_sup, round_sup], color='r', lw=3, ls='--')\n",
+ "ax2.plot([xlim0, xlim1], [round_inf, round_inf], color='r', lw=3, ls='--')\n",
+ "ax2.plot([lmag_lim, lmag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "ax2.plot([umag_lim, umag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "\n",
+ "plt.tight_layout()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6ac852af",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "mask = ((found_stars['mag'] < lmag_lim) & (found_stars['mag'] > umag_lim) & (found_stars['roundness2'] > round_inf)\n",
+ " & (found_stars['roundness2'] < round_sup) & (found_stars['sharpness'] > sh_inf) \n",
+ " & (found_stars['sharpness'] < sh_sup) & (found_stars['xcentroid'] > 100) & (found_stars['xcentroid'] < 700)\n",
+ " & (found_stars['ycentroid'] > 100) & (found_stars['ycentroid'] < 700))\n",
+ "\n",
+ "found_stars_sel = found_stars[mask]\n",
+ "\n",
+ "print('Number of stars found originally:', len(found_stars))\n",
+ "print('Number of stars in final selection:', len(found_stars_sel))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "567f81f5",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "found_stars_sel"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7fd293e-a8f4-4c34-ab99-23d71adee080",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Convert pixel to wcs coords\n",
+ "skycoords = w.pixel_to_world(found_stars_sel['xcentroid'], found_stars_sel['ycentroid'])\n",
+ "len(skycoords)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6c46e19",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "for file in lvl2:\n",
+ " ref_fits = ImageModel(file)\n",
+ " data = ref_fits.data\n",
+ " dq = ref_fits.dq\n",
+ " data[dq >= 10] = np.nan\n",
+ " ref_fits.data = data\n",
+ " ref_fits.save(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5516a64f",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create a grid for fast lookup using WebbPSF. The larger the number of grid points, the better the photometric precision.\n",
+ "# Developer note. Would be great to have a fast/approximate look up table. \n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "grid = space_phot.util.get_jwst_psf_grid(jwst_obs, num_psfs=4)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "38760103-e702-4b6b-bd5e-7ed12c67a6d8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = QTable([skycoords], names=[\"skycoord\"])\n",
+ "t.write('skycoord.ecsv', overwrite=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b85e222f",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now Loop Through All Stars and Build Photometry Table\n",
+ "# Readers should refer to all diagnostics discussed above. \n",
+ "# It should be noted that empty plots correspond to LVL2 files with dither positions that do not cover that particular coordinate.\n",
+ "counter = 0.\n",
+ "badindex = []\n",
+ "\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "for source_location in skycoords:\n",
+ " tic = time.perf_counter()\n",
+ " print('Starting', counter+1., ' of', len(skycoords), ':', source_location)\n",
+ " psfs = space_phot.util.get_jwst_psf_from_grid(jwst_obs, source_location, grid)\n",
+ " jwst_obs.psf_photometry(psfs, source_location, bounds={'flux': [-100000, 100000],\n",
+ " 'centroid': [-2., 2.],\n",
+ " 'bkg': [0, 50]},\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_flux='single',\n",
+ " maxiter=10000)\n",
+ " \n",
+ " jwst_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ " \n",
+ " ra = jwst_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " mag_arr = jwst_obs.psf_result.phot_cal_table['mag']\n",
+ " magerr_arr = jwst_obs.psf_result.phot_cal_table['magerr']\n",
+ " mag_lvl2psf = np.mean(mag_arr)\n",
+ " magerr_lvl2psf = math.sqrt(sum(p**2 for p in magerr_arr))\n",
+ "\n",
+ " if counter == 0:\n",
+ " df = pd.DataFrame(np.array([[ra, dec, mag_lvl2psf, magerr_lvl2psf]]), columns=['ra', 'dec', 'mag', 'magerr'])\n",
+ " else:\n",
+ " df = pd.concat([df, pd.DataFrame(np.array([[ra, dec, mag_lvl2psf, magerr_lvl2psf]]), columns=['ra', 'dec', 'mag', 'magerr'])], ignore_index=True)\n",
+ " counter = counter + 1.\n",
+ " \n",
+ " toc = time.perf_counter()\n",
+ " print(\"Elapsed Time for Photometry:\", toc - tic)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d4fbc1e5-96c5-48bb-b463-5af8970f1a2f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3604e260-2da4-4f43-b306-fb7cd65e738b",
+ "metadata": {},
+ "source": [
+ "### 5.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a57e893d-92cb-4de6-8c64-69911b691246",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "lvl2[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24dbbba6-6d1a-40b2-9028-de916cdc76e4",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=0.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5be212d8-c43a-478e-98ed-b1877e44a347",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF and drizzle it to the source location\n",
+ "jwst3_obs = space_phot.observation3(lvl3[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e4c85327-4b59-4228-8610-4a85909fb397",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "lvl3[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "717c2fc5-1e72-48c7-98aa-4b8216aac567",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "skycoords"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1881aeea-d5c7-4e1c-9669-19351f9c1157",
+ "metadata": {},
+ "source": [
+ "#### Readers should refer to all diagnostics discussed above. In general, this loop shows the difficulty in doing PSF photometry on a wide variety of stars (brightness, distribution on the detector, etc) without visual inspection. Especially when dealing with low SNR sources.This is true for all photometry packages. Users should inspect the associated metrics and consider optimizing the parameters for specific stars of interest. Nonetheless, the success of the fits can always be quantified in with the associated error bars.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "922accc4-2179-4e03-ad60-2beeb594faea",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now Loop Through All Stars and Build Photometry Table\n",
+ "counter = 0.\n",
+ "badindex = []\n",
+ "\n",
+ "for source_location in skycoords:\n",
+ " tic = time.perf_counter()\n",
+ " print('Starting', counter+1., ' of', len(skycoords), ':', source_location)\n",
+ " psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location, num_psfs=4)\n",
+ " jwst3_obs.psf_photometry(psf3, source_location, bounds={'flux': [-10000, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50]},\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_flux=True)\n",
+ "\n",
+ " jwst3_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ "\n",
+ " ra = jwst3_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst3_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " mag_lvl3psf = jwst3_obs.psf_result.phot_cal_table['mag'][0]\n",
+ " magerr_lvl3psf = jwst3_obs.psf_result.phot_cal_table['magerr'][0]\n",
+ "\n",
+ " if counter == 0:\n",
+ " df = pd.DataFrame(np.array([[ra, dec, mag_lvl3psf, magerr_lvl3psf]]), columns=['ra', 'dec', 'mag', 'magerr'])\n",
+ " else:\n",
+ " df = pd.concat([df, pd.DataFrame(np.array([[ra, dec, mag_lvl3psf, magerr_lvl3psf]]), columns=['ra', 'dec', 'mag', 'magerr'])], ignore_index=True)\n",
+ " counter = counter + 1.\n",
+ " toc = time.perf_counter()\n",
+ " print(\"Elapsed Time for Photometry:\", toc - tic)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "db250a73-d9a6-4f38-a42b-335f66d73bde",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4cb0557a-dee2-4be3-9513-83bb9671d71e",
+ "metadata": {},
+ "source": [
+ "**Important Note**: When not to use. Due to the sensitivity of the space_phot parameters, this tool is not meant to be used for a large sample of stars (i.e., Section 5 below). If a user would like to use space_phot on more than one source, they should carefully construct a table of parameters that are carefully refined for each source."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5af6df8c-85ea-4aac-b6b7-53ac630fa3e0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5630029f-31d1-42cd-8454-225e86cabc48",
+ "metadata": {},
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "843b5201-6f57-46f0-9da0-b738714178d3",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ },
+ "toc-showcode": false
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/MIRI/psf_photometry/miri_1028_photutils.ipynb b/notebooks/MIRI/psf_photometry/miri_1028_photutils.ipynb
new file mode 100644
index 000000000..dc71ca111
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/miri_1028_photutils.ipynb
@@ -0,0 +1,1062 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b58d6954",
+ "metadata": {},
+ "source": [
+ "# MIRI PSF Photometry with Photutils\n",
+ "\n",
+ "**Author**: Ori Fox
\n",
+ "\n",
+ "**Submitted**: November, 2023
\n",
+ "**Updated**: November, 2023
\n",
+ "\n",
+ "**Use case**: PSF Photometry using [Photutils](https://photutils.readthedocs.io/en/stable/). The purpose here is to illustrate the workflow and runtime for using Photutils in a variety of use cases.\n",
+ "\n",
+ "Generally, PSF photometry for data from a space telescope is most accurately performed on pre-mosaiced data. The mosaic process changes the inherent PSF, blurring it both due to resampling and mixing PSFs at different detector positions and rotations. Additionally, accurate theoretical PSF models (e.g., from [WebbPSF](https://webbpsf.readthedocs.io/en/latest/)) are not available for mosaiced data. While an empirical PSF could be constructed (e.g., using Photutils [ePSFBuilder](https://photutils.readthedocs.io/en/latest/epsf.html)) for mosaiced data, the results will generally not be as accurate as performing PSF photometry on the pre-mosaiced data.\n",
+ "\n",
+ "**NOTE:** A companion notebook exists that illustrates how to use perform PSF photometry on both Level 2 and Level 3 data using a new software program called space_phot.
\n",
+ "**Data**: MIRI Data PID 1028 (Calibration Program; Single Star Visit 006 A5V dwarf 2MASSJ17430448+6655015) and MIRI Data PID 1171 (LMC; Multiple Stars).
\n",
+ "**Tools**: photutils, webbpsf, jwst
\n",
+ "**Cross-Instrument**: MIRI
\n",
+ "**Documentation:** This notebook is part of a STScI's larger [post-pipeline Data Analysis Tools Ecosystem](https://jwst-docs.stsci.edu/jwst-post-pipeline-data-analysis) and can be [downloaded](https://github.com/spacetelescope/dat_pyinthesky/tree/main/jdat_notebooks/MRS_Mstar_analysis) directly from the [JDAT Notebook Github directory](https://github.com/spacetelescope/jdat_notebooks).
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "88c61bcf-1c4d-407a-b80c-aa13a01fd746",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Table of contents\n",
+ "1. [Introduction](#intro)
\n",
+ " 1.1 [Python Imports](#imports)
\n",
+ " 1.2 [Set up WebbPSF and Synphot](#setup)
\n",
+ "2. [Download JWST MIRI Data](#data)
\n",
+ "3. [Single Bright Object](#bso)
\n",
+ " 3.1 [Single Level 2 File](#bso2)
\n",
+ " 3.2 [Generate empirical PSF grid for MIRI F770W using WebbPSF](#bso3)
\n",
+ " 3.3 [PSF Photometry](#bso4)
\n",
+ "4. [Faint/Upper Limit, Single Object](#fso)
\n",
+ " 4.1 [Multiple, Level2 Files](#fso2)
\n",
+ "5. [Stellar Field (LMC)](#lmc)
\n",
+ " 5.1 [Multiple Stars, Single Level 2 File](#lmc2)
\n",
+ " 5.2 [Generate empirical PSF grid for MIRI F560W using WebbPSF](#grid2)
\n",
+ " 5.3 [PSF Photometry](#lmc3)
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f572688",
+ "metadata": {},
+ "source": [
+ "# 1. Introduction "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "95891849",
+ "metadata": {},
+ "source": [
+ "**GOALS**:
\n",
+ "\n",
+ "Perform PSF photometry on JWST MIRI images with the [Photutils PSF Photometry tools](https://photutils.readthedocs.io/en/latest/psf.html) using a grid of empirical PSF models from WebbPSF.\n",
+ "\n",
+ "\n",
+ "The notebook shows how to:
\n",
+ "\n",
+ "* generate a [grid of empirical PSF models](https://webbpsf.readthedocs.io/en/latest/psf_grids.html) from WebbPSF
\n",
+ "* perform PSF photometry on the image using the [PSFPhotometry class](https://photutils.readthedocs.io/en/latest/api/photutils.psf.PSFPhotometry.html#photutils.psf.PSFPhotometry)
\n",
+ "\n",
+ "**Data**:
\n",
+ "\n",
+ "MIRI Data PID 1028 (Calibration Program), F770W
\n",
+ "MIRI Data PID 1171 (LMC), F560W/F770W"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5e534877-5c31-4020-9263-4f234f19e1cd",
+ "metadata": {},
+ "source": [
+ "## 1.1 Python Imports "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1ca81097-08d5-470c-942a-d8e7e8fd4479",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import glob\n",
+ "import os\n",
+ "import shutil\n",
+ "import tarfile\n",
+ "\n",
+ "import astropy.units as u\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "import webbpsf\n",
+ "import requests\n",
+ "from urllib.parse import urlparse\n",
+ "from astropy.coordinates import SkyCoord\n",
+ "from astropy.io import fits\n",
+ "from astropy.nddata import extract_array\n",
+ "from astropy.table import Table\n",
+ "from astropy.visualization import simple_norm\n",
+ "from astroquery.mast import Observations\n",
+ "from jwst.datamodels import ImageModel\n",
+ "from photutils.aperture import CircularAperture\n",
+ "from photutils.background import LocalBackground, MADStdBackgroundRMS, MMMBackground\n",
+ "from photutils.detection import DAOStarFinder\n",
+ "from photutils.psf import GriddedPSFModel, PSFPhotometry"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b762602",
+ "metadata": {},
+ "source": [
+ "## 1.2 Download and Set up Required Data for WebbPSF and Synphot "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c50eace",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Set environmental variables\n",
+ "os.environ[\"WEBBPSF_PATH\"] = \"./webbpsf-data/webbpsf-data\"\n",
+ "os.environ[\"PYSYN_CDBS\"] = \"./grp/redcat/trds/\"\n",
+ "\n",
+ "# required webbpsf data\n",
+ "boxlink = 'https://stsci.box.com/shared/static/qxpiaxsjwo15ml6m4pkhtk36c9jgj70k.gz' \n",
+ "boxfile = './webbpsf-data/webbpsf-data-LATEST.tar.gz'\n",
+ "synphot_url = 'http://ssb.stsci.edu/trds/tarfiles/synphot5.tar.gz'\n",
+ "synphot_file = './synphot5.tar.gz'\n",
+ "\n",
+ "webbpsf_folder = './webbpsf-data'\n",
+ "synphot_folder = './grp'\n",
+ "\n",
+ "\n",
+ "def download_file(url, dest_path, timeout=60):\n",
+ " parsed_url = urlparse(url)\n",
+ " if parsed_url.scheme not in [\"http\", \"https\"]:\n",
+ " raise ValueError(f\"Unsupported URL scheme: {parsed_url.scheme}\")\n",
+ "\n",
+ " response = requests.get(url, stream=True, timeout=timeout)\n",
+ " response.raise_for_status()\n",
+ " with open(dest_path, \"wb\") as f:\n",
+ " for chunk in response.iter_content(chunk_size=8192):\n",
+ " f.write(chunk)\n",
+ "\n",
+ "\n",
+ "# Gather webbpsf files\n",
+ "psfExist = os.path.exists(webbpsf_folder)\n",
+ "if not psfExist:\n",
+ " os.makedirs(webbpsf_folder)\n",
+ " download_file(boxlink, boxfile)\n",
+ " gzf = tarfile.open(boxfile)\n",
+ " gzf.extractall(webbpsf_folder, filter='data')\n",
+ "\n",
+ "# Gather synphot files\n",
+ "synExist = os.path.exists(synphot_folder)\n",
+ "if not synExist:\n",
+ " os.makedirs(synphot_folder)\n",
+ " download_file(synphot_url, synphot_file)\n",
+ " gzf = tarfile.open(synphot_file)\n",
+ " gzf.extractall('./', filter='data')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "68f0b2d7-45a1-4511-858e-51425a50de00",
+ "metadata": {},
+ "source": [
+ "# 2. Download JWST MIRI Data "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6629878-a5d4-4e29-a56e-0f12271016a5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Download Proposal ID 1028 F770W data\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01028/'\n",
+ "\n",
+ "if os.path.isdir(destination_dir):\n",
+ " print(f'Data already downloaded to {os.path.abspath(destination_dir)}')\n",
+ "else:\n",
+ " # Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ " # with proposal ID 1028 and the F770W filter\n",
+ " obs = Observations.query_criteria(proposal_id=1028, filters=['F770W'])\n",
+ " \n",
+ " # Get a list of products associated with the located observation\n",
+ " plist = Observations.get_product_list(obs)\n",
+ " \n",
+ " # Filter the product list to include only specific product subgroups\n",
+ " fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ " \n",
+ " # Download the selected products from the MAST database\n",
+ " Observations.download_products(fplist)\n",
+ " \n",
+ " # Create the destination directory\n",
+ " os.makedirs(destination_dir)\n",
+ " \n",
+ " # Use glob to find all files matching the pattern\n",
+ " files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01028*'))\n",
+ "\n",
+ " # Copy the matching files to the destination directory\n",
+ " for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b649b6f4-aa18-4760-a7d0-979b4e3caec2",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "# Download Proposal ID 1171 F560W and F770W data\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01171/'\n",
+ "\n",
+ "if os.path.isdir(destination_dir):\n",
+ " print(f'Data already downloaded to {os.path.abspath(destination_dir)}')\n",
+ "else:\n",
+ " # Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ " # with proposal ID 1171 and the F550W and F770W filters\n",
+ " obs = Observations.query_criteria(proposal_id=1171, filters=['F560W', 'F770W'])\n",
+ " \n",
+ " # Get a list of products associated with the located observation\n",
+ " plist = Observations.get_product_list(obs)\n",
+ " \n",
+ " # Filter the product list to include only specific product subgroups\n",
+ " fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ " \n",
+ " # Download the selected products from the MAST database\n",
+ " Observations.download_products(fplist)\n",
+ " \n",
+ " # Create the destination directory\n",
+ " os.makedirs(destination_dir)\n",
+ " \n",
+ " # Use glob to find all files matching the pattern\n",
+ " files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01171*'))\n",
+ " \n",
+ " # Copy the matching files to the destination directory\n",
+ " for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5611799d",
+ "metadata": {},
+ "source": [
+ "# 3. Single Bright Star "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6d052f0e-dcb4-4c2a-bc11-4b467dad07c2",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to perform PSF photometry on a single bright star. While aperture photometry is feasible in isolated cases, the user may find PSF photometry preferable in crowded fields or complicated backgrounds."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55c52f95",
+ "metadata": {},
+ "source": [
+ "## 3.1 Single Level 2 File "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "058a14ad-be89-4d7e-934e-1e0a909319c8",
+ "metadata": {},
+ "source": [
+ "In this example, we fit a single, bright source in a single Level 2 images. For a collection of Level 2 images, we could fit each Level 2 image individually and then average the measured fluxes.\n",
+ "\n",
+ "Useful references:
\n",
+ "HST Documentation on PSF Photometry: https://www.stsci.edu/hst/instrumentation/wfc3/data-analysis/psf
\n",
+ "WFPC2 Stellar Photometry with HSTPHOT: https://ui.adsabs.harvard.edu/abs/2000PASP..112.1383D/abstract
\n",
+ "Photutils PSF Fitting Photometry: https://photutils.readthedocs.io/en/stable/psf.html"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2af76a35-14ee-44b2-adb5-d92e66ddfafc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# get the level 2 filenames\n",
+ "path = \"./mast/01028/\"\n",
+ "level2 = sorted(glob.glob(os.path.join(path, '*cal.fits')))\n",
+ "level2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab0ac799-a832-40af-9cd4-b5b3934cfee4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# display the first level-2 image\n",
+ "data = fits.getdata(level2[0])\n",
+ "norm = simple_norm(data, 'sqrt', percent=99)\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(20, 12))\n",
+ "im = ax.imshow(data, origin='lower', norm=norm, cmap='gray')\n",
+ "clb = plt.colorbar(im, label='MJy/sr')\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ab947da-be0a-4cde-88eb-2d947adf2b81",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NaN.\n",
+ "# Here, we'll overwrite the original CAL file.\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "filename = level2[0]\n",
+ "with ImageModel(filename) as model:\n",
+ " model.data[model.dq >= 10] = np.nan\n",
+ " model.save(filename)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "87673dd8-1bc6-4663-bebc-8e2434974ceb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Re-display the image\n",
+ "data = fits.getdata(level2[0])\n",
+ "norm = simple_norm(data, 'sqrt', percent=99)\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(20, 12))\n",
+ "im = ax.imshow(data, origin='lower', norm=norm, cmap='gray')\n",
+ "clb = plt.colorbar(im, label='MJy/sr')\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2f138285-bfc5-4f20-854b-fe5d4530e99b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source. In this case, our source is from MIRI Program ID #1028, a Calibration Program.\n",
+ "# We are using Visit 006, which targets the A5V dwarf 2MASSJ17430448+6655015\n",
+ "# Reference Link: http://simbad.cds.unistra.fr/simbad/sim-basic?Ident=2MASSJ17430448%2B6655015&submit=SIMBAD+search\n",
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "with ImageModel(filename) as model:\n",
+ " x, y = model.meta.wcs.world_to_pixel(source_location)\n",
+ "\n",
+ "cutout = extract_array(data, (21, 21), (y, x))\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(8, 8))\n",
+ "norm2 = simple_norm(cutout, 'log', percent=99)\n",
+ "im = ax.imshow(cutout, origin='lower', norm=norm2, cmap='gray')\n",
+ "clb = plt.colorbar(im, label='MJy/sr', shrink=0.8)\n",
+ "ax.set_title('PID1028, Obs006')\n",
+ "\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b24288b6-c6c7-433f-866f-126eea4a9ab3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2024-02-10T00:33:17.900216Z",
+ "iopub.status.busy": "2024-02-10T00:33:17.899831Z",
+ "iopub.status.idle": "2024-02-10T00:33:17.903626Z",
+ "shell.execute_reply": "2024-02-10T00:33:17.902732Z",
+ "shell.execute_reply.started": "2024-02-10T00:33:17.900187Z"
+ }
+ },
+ "source": [
+ "## 3.2 Generate empirical PSF grid for MIRI F770W using WebbPSF "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f7472c4e-f418-4f39-b8b6-5d824c1c4c65",
+ "metadata": {},
+ "source": [
+ "Let's now use WebbPSF to generate an empirical grid of ePSF models for MIRI F770W.\n",
+ "The output will be a Photutils [GriddedPSFModel](https://photutils.readthedocs.io/en/latest/api/photutils.psf.GriddedPSFModel.html#photutils.psf.GriddedPSFModel) containing a 2x2 grid of detector-position-dependent empirical PSFs, each oversampled by a factor of 4. Note that we save the PSF grid to a FITS file (via `save=True`) called `miri_mirim_f770w_fovp101_samp4_npsf4.fits`. To save time in future runs, we load this FITS file directly into a `GriddedPSFModel` object:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "19b97fc8-35b4-460f-b154-72bdd4c45f66",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psfgrid_filename = 'miri_mirim_f770w_fovp101_samp4_npsf4.fits'\n",
+ "\n",
+ "if not os.path.exists(psfgrid_filename):\n",
+ " miri = webbpsf.MIRI()\n",
+ " miri.filter = 'F770W'\n",
+ " psf_model = miri.psf_grid(num_psfs=4, all_detectors=True, verbose=True, save=True)\n",
+ "else:\n",
+ " psf_model = GriddedPSFModel.read(psfgrid_filename)\n",
+ "\n",
+ "psf_model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7d1640dc-6c64-4fe7-a208-ddbcdc529512",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# display the PSF grid\n",
+ "psf_model.plot_grid()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7294300-6ba8-418b-a61c-4194af270de3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# display the PSF grid deltas from the mean ePSF\n",
+ "psf_model.plot_grid(deltas=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bcab882b-16f6-4aef-9f4f-b3af8b3b1b14",
+ "metadata": {},
+ "source": [
+ "## 3.3 PSF Photometry "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b34c6e74-1a52-4874-afc1-5a444662e966",
+ "metadata": {},
+ "source": [
+ "Now let's use our gridded PSF model to perform PSF photometry."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f42d294f-a5db-4a2a-be4a-e40a26374207",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# load data and convert units from MJy/sr to uJy\n",
+ "with ImageModel(filename) as model:\n",
+ " unit = u.Unit(model.meta.bunit_data)\n",
+ " data = model.data << unit\n",
+ " error = model.err << unit\n",
+ "\n",
+ " # use pixel area map because of geometric distortion in level-2 data \n",
+ " pixel_area = model.area * model.meta.photometry.pixelarea_steradians * u.sr\n",
+ " data *= pixel_area\n",
+ " error *= pixel_area\n",
+ " \n",
+ " data = data.to(u.uJy)\n",
+ " error = error.to(u.uJy)\n",
+ "\n",
+ "data.unit, error.unit"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "318ab078-ec51-4b71-9641-dd1fe566138a",
+ "metadata": {},
+ "source": [
+ "To perform photometry on a single source we can input a Table containing its (x, y) position."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6efc06f3-445d-4139-a404-fff43ad15804",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "init_params = Table()\n",
+ "init_params['x'] = [x]\n",
+ "init_params['y'] = [y]\n",
+ "init_params"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "61424bfb-7c76-489b-9ccf-efc9c324472d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# we turn off the finder because we input the source position\n",
+ "fit_shape = 5\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "psfphot = PSFPhotometry(psf_model, fit_shape, finder=None, aperture_radius=fit_shape, \n",
+ " localbkg_estimator=localbkg_estimator, progress_bar=True)\n",
+ "phot = psfphot(data, error=error, init_params=init_params)\n",
+ "phot"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "62945e0b-6eb7-487f-a7b6-8e3c718b3c26",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# convert fit flux from uJy to ABmag\n",
+ "flux = phot['flux_fit']\n",
+ "flux_err = phot['flux_err']\n",
+ "mag = phot['flux_fit'].to(u.ABmag)\n",
+ "magerr = 2.5 * np.log10(1.0 + (flux_err / flux))\n",
+ "magerr = magerr.value * u.ABmag\n",
+ "mag, magerr"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6c8baf43-51bc-4ff5-97e9-10db55ab6128",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig, ax = plt.subplots(ncols=3, figsize=(12, 4))\n",
+ "\n",
+ "shape = (21, 21)\n",
+ "cutout1 = extract_array(data.value, shape, (y, x))\n",
+ "norm = simple_norm(cutout1, 'log', percent=98)\n",
+ "im1 = ax[0].imshow(cutout1, origin='lower', norm=norm)\n",
+ "ax[0].set_title(r'Data ($\\mu$Jy)')\n",
+ "plt.colorbar(im1, shrink=0.7)\n",
+ "\n",
+ "model = psfphot.make_model_image(data.shape, shape)\n",
+ "cutout2 = extract_array(model, shape, (y, x))\n",
+ "im2 = ax[1].imshow(cutout2, origin='lower', norm=norm)\n",
+ "ax[1].set_title('Fit PSF Model')\n",
+ "plt.colorbar(im2, shrink=0.7)\n",
+ "\n",
+ "resid = psfphot.make_residual_image(data.value, shape)\n",
+ "cutout3 = extract_array(resid, shape, (y, x))\n",
+ "norm3 = simple_norm(cutout3, 'sqrt', percent=99)\n",
+ "im3 = ax[2].imshow(cutout3, origin='lower', norm=norm3)\n",
+ "ax[2].set_title('Residual')\n",
+ "plt.colorbar(im3, shrink=0.7)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b5f0ad5-b59e-4eff-8687-f6a2199d8bd9",
+ "metadata": {},
+ "source": [
+ "# 4. Faint/Upper Limit, Single Object "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1dc60da1-0f4d-4e5e-a109-f7352dfd0fdc",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to calculate an upper limit at a fixed (x, y) position using forced PSF photometry a blank part of the sky.\n",
+ "\n",
+ "We'll use the same data as Section 3."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9311116e-630b-4a2e-8fc6-be75f6de61fd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# load data and convert units from MJy/sr to uJy\n",
+ "with ImageModel(filename) as model:\n",
+ " unit = u.Unit(model.meta.bunit_data)\n",
+ " data = model.data << unit\n",
+ " error = model.err << unit\n",
+ " \n",
+ " pixel_area = pixel_area = model.meta.photometry.pixelarea_steradians * u.sr\n",
+ " data *= pixel_area\n",
+ " error *= pixel_area\n",
+ " \n",
+ " data = data.to(u.uJy)\n",
+ " error = error.to(u.uJy)\n",
+ "\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "with ImageModel(filename) as model:\n",
+ " x, y = model.meta.wcs.world_to_pixel(source_location)\n",
+ "\n",
+ "cutout = extract_array(data.value, (21, 21), (y, x))\n",
+ "\n",
+ "fig, ax = plt.subplots()\n",
+ "norm = simple_norm(cutout, 'sqrt', percent=95)\n",
+ "im = ax.imshow(cutout, origin='lower', norm=norm, cmap='gray')\n",
+ "clb = plt.colorbar(im, label=r'$\\mu$Jy')\n",
+ "ax.set_title('PID1028, Obs006')\n",
+ "\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "63ac28fd-ad3b-479b-8561-ca5913925aba",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# to perform forced photometry, we set the (x, y) source position\n",
+ "# AND we fix the PSF model position so that it does not vary in the fit\n",
+ "# (only flux will be fit)\n",
+ "init_params = Table()\n",
+ "init_params['x'] = [x]\n",
+ "init_params['y'] = [y]\n",
+ "\n",
+ "# This requires photutils 1.11.0\n",
+ "psf_model_forced = psf_model.copy()\n",
+ "psf_model_forced.x_0.fixed = True\n",
+ "psf_model_forced.y_0.fixed = True\n",
+ "psf_model_forced.fixed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a13930cb-fd2a-447e-a68f-357eb7014ad3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fit_shape = 5\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "psfphot = PSFPhotometry(psf_model_forced, fit_shape, finder=None, aperture_radius=fit_shape, \n",
+ " localbkg_estimator=localbkg_estimator, progress_bar=True)\n",
+ "\n",
+ "phot = psfphot(data, error=error, init_params=init_params)\n",
+ "phot"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3206847e-2e2e-4bc3-a51e-3e5877f87b28",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# To calculate upper limit, multiply the flux_err by your desired sigma\n",
+ "sigma = 3.0\n",
+ "limit = sigma * phot['flux_err']\n",
+ "limit.to(u.ABmag)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e4996d2-6274-473d-b13c-f3848c27ad78",
+ "metadata": {},
+ "source": [
+ "## Note: you can go significantly deeper with the Level 3 combined data product"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a969717-bbef-40b9-ac9b-f83dec99dc09",
+ "metadata": {},
+ "source": [
+ "# 5. Stellar Field (LMC) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "da877310-fd47-41d6-afea-7fa725a546af",
+ "metadata": {},
+ "source": [
+ "In this case, we are going to do the same steps as in Section 3, but for multiple stars. The purpose is to illustrate the workflow and runtime for using Photutils on a large number of stars."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32bdafe6-db19-4080-9587-b9785c2f7fa7",
+ "metadata": {},
+ "source": [
+ "## 5.1 Multiple Stars, Single Level 2 File "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "72aabedd-8d80-4c6e-8d3d-9287577665bf",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Find stars in Level 3 File\n",
+ "path = './mast/01171/'\n",
+ "level3 = os.path.join(path, 'jw01171-o004_t001_miri_f560w_i2d.fits')\n",
+ "level3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4eb6155b-5f1a-4f92-9c34-5cd0876e439d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get rough estimate of background (there are better ways to do background subtraction)\n",
+ "bkgrms = MADStdBackgroundRMS()\n",
+ "mmm_bkg = MMMBackground()\n",
+ "\n",
+ "with ImageModel(level3) as model:\n",
+ " wcs_l3 = model.meta.wcs\n",
+ " std = bkgrms(model.data)\n",
+ " bkg = mmm_bkg(model.data)\n",
+ " data_bkgsub = model.data.copy()\n",
+ " data_bkgsub -= bkg \n",
+ "\n",
+ "# Find stars\n",
+ "# F560W FWHM = 1.882 pix\n",
+ "fwhm_psf = 1.882\n",
+ "threshold = 5.0\n",
+ "daofind = DAOStarFinder(threshold=threshold * std, fwhm=fwhm_psf, exclude_border=True, min_separation=10)\n",
+ "found_stars = daofind(data_bkgsub)\n",
+ "\n",
+ "# print first 10 rows\n",
+ "found_stars[:10]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "73aba802",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# plot the found stars\n",
+ "norm = simple_norm(data_bkgsub, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data_bkgsub, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(found_stars['xcentroid'], found_stars['ycentroid'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "84411432-3270-47de-a546-a81935c19c5c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig, ax = plt.subplots(nrows=2, figsize=(10, 8))\n",
+ "\n",
+ "ax[0].scatter(found_stars['mag'], found_stars['sharpness'], s=10, color='k')\n",
+ "ax[0].set_xlabel('mag')\n",
+ "ax[0].set_ylabel('sharpness')\n",
+ "\n",
+ "ax[1].scatter(found_stars['mag'], found_stars['roundness2'], s=10, color='k')\n",
+ "ax[1].set_xlabel('mag')\n",
+ "ax[1].set_ylabel('roundness')\n",
+ "\n",
+ "mag0 = -3.0\n",
+ "mag1 = -5.0\n",
+ "for ax_ in ax:\n",
+ " ax_.axvline(mag0, color='red', linestyle='dashed')\n",
+ " ax_.axvline(mag1, color='red', linestyle='dashed')\n",
+ "\n",
+ "sh0 = 0.40\n",
+ "sh1 = 0.82\n",
+ "ax[0].axhline(sh0, color='red', linestyle='dashed')\n",
+ "ax[0].axhline(sh1, color='red', linestyle='dashed')\n",
+ "\n",
+ "rnd0 = -0.40\n",
+ "rnd1 = 0.40\n",
+ "ax[1].axhline(rnd0, color='red', linestyle='dashed')\n",
+ "ax[1].axhline(rnd1, color='red', linestyle='dashed')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "04f2dbae-1b1c-4f67-902f-eb91f73b2d6d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mask = ((found_stars['mag'] < mag0) & (found_stars['mag'] > mag1) & (found_stars['roundness2'] > rnd0)\n",
+ " & (found_stars['roundness2'] < rnd1) & (found_stars['sharpness'] > sh0) \n",
+ " & (found_stars['sharpness'] < sh1) & (found_stars['xcentroid'] > 100) & (found_stars['xcentroid'] < 700)\n",
+ " & (found_stars['ycentroid'] > 100) & (found_stars['ycentroid'] < 700))\n",
+ "\n",
+ "found_stars_sel = found_stars[mask]\n",
+ "\n",
+ "print('Number of stars found originally:', len(found_stars))\n",
+ "print('Number of stars in final selection:', len(found_stars_sel))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "948f0a96-8bdc-4c94-95db-8edf513e4094",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot the selected stars\n",
+ "norm = simple_norm(data_bkgsub, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data_bkgsub, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(found_stars_sel['xcentroid'], found_stars_sel['ycentroid'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a221246a-9acb-4c79-9831-2f6cb72bb90a",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2024-02-10T02:52:51.761591Z",
+ "iopub.status.busy": "2024-02-10T02:52:51.761230Z",
+ "iopub.status.idle": "2024-02-10T02:52:51.765256Z",
+ "shell.execute_reply": "2024-02-10T02:52:51.764441Z",
+ "shell.execute_reply.started": "2024-02-10T02:52:51.761562Z"
+ }
+ },
+ "source": [
+ "## 5.2 Generate empirical PSF grid for MIRI F560W using WebbPSF "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b5fdcf15-7f72-40b8-b1aa-2b5fb0375ed8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psfgrid_filename = 'miri_mirim_f560w_fovp101_samp4_npsf4.fits'\n",
+ "\n",
+ "if not os.path.exists(psfgrid_filename):\n",
+ " miri = webbpsf.MIRI()\n",
+ " miri.filter = 'F560W'\n",
+ " psf_model = miri.psf_grid(num_psfs=4, all_detectors=True, verbose=True, save=True)\n",
+ "else:\n",
+ " psf_model = GriddedPSFModel.read(psfgrid_filename)\n",
+ "\n",
+ "psf_model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29fa22f3-0331-4966-9bb4-c75f325011c8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psf_model.plot_grid()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b2350526-2bba-4322-9092-88b6acdc117b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# get the level 2 image\n",
+ "# here, we'll use the PID 1171 files\n",
+ "path = \"./mast/01171/\"\n",
+ "level2 = sorted(glob.glob(os.path.join(path, 'jw01171004*cal.fits')))\n",
+ "filename = level2[0]\n",
+ "print(filename)\n",
+ "\n",
+ "# load data and convert units from MJy/sr to uJy\n",
+ "with ImageModel(filename) as model:\n",
+ " unit = u.Unit(model.meta.bunit_data)\n",
+ " model.data[model.dq >= 10] = np.nan\n",
+ " data = model.data << unit\n",
+ " error = model.err << unit\n",
+ " \n",
+ " pixel_area = pixel_area = model.meta.photometry.pixelarea_steradians * u.sr\n",
+ " data *= pixel_area\n",
+ " error *= pixel_area\n",
+ " \n",
+ " data = data.to(u.uJy)\n",
+ " error = error.to(u.uJy)\n",
+ "\n",
+ " wcs = model.meta.wcs\n",
+ "\n",
+ "data.unit, error.unit"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9aa77cc2-26e2-4ac3-8384-90b9f8567f22",
+ "metadata": {},
+ "source": [
+ "## 5.3 PSF Photometry "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3b416d45-1081-4a16-b189-cc0e5e9d4e4e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# translate (x, y) positions from the level 3 image to the level 2 image\n",
+ "xc = found_stars_sel['xcentroid']\n",
+ "yc = found_stars_sel['ycentroid']\n",
+ "sc = wcs_l3.pixel_to_world(xc, yc)\n",
+ "\n",
+ "x, y = wcs.world_to_pixel(sc)\n",
+ "init_params = Table()\n",
+ "init_params['x'] = x\n",
+ "init_params['y'] = y\n",
+ "\n",
+ "# we need to remove stars in the masked region of\n",
+ "# the level-2 data\n",
+ "mask = x > 400\n",
+ "init_params = init_params[mask]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f17e393a-9f0b-4b83-83b9-4bb8497dd835",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mask"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "511183e0-0adc-4966-b7f4-fcd912b054e9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot the selected stars\n",
+ "norm = simple_norm(data.value, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data.value, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(init_params['x'], init_params['y'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "84812421-f447-4b75-9fe4-e377fba54b17",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fit_shape = 5\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "psfphot = PSFPhotometry(psf_model, fit_shape, finder=None, aperture_radius=fit_shape, \n",
+ " localbkg_estimator=localbkg_estimator, progress_bar=True)\n",
+ "phot = psfphot(data, error=error, init_params=init_params)\n",
+ "phot"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8d5899cf-8956-4b3c-941a-67b0fedc0c51",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# convert fit flux from uJy to ABmag\n",
+ "flux = phot['flux_fit']\n",
+ "flux_err = phot['flux_err']\n",
+ "mag = phot['flux_fit'].to(u.ABmag)\n",
+ "magerr = 2.5 * np.log10(1.0 + (flux_err / flux))\n",
+ "magerr = magerr.value * u.ABmag\n",
+ "mag, magerr"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5630029f-31d1-42cd-8454-225e86cabc48",
+ "metadata": {},
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "843b5201-6f57-46f0-9da0-b738714178d3",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ },
+ "toc-showcode": false
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/MIRI/psf_photometry/miri_photometry_photutils.txt b/notebooks/MIRI/psf_photometry/miri_photometry_photutils.txt
new file mode 100644
index 000000000..23b97d7b2
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/miri_photometry_photutils.txt
@@ -0,0 +1,12 @@
+RA,DEC,Mag,Mag Err
+80.70519786193717,-69.43753355085106,19.416364334972236,0.006262633729496935
+80.69947280902996,-69.44357618680897,18.708534170910088,0.0045114083804914835
+80.70076349113891,-69.44322911964474,18.963787339369823,0.005092203034236376
+80.70480666335257,-69.44154063210084,19.108665874923563,0.005125374357902258
+80.70685608541822,-69.44434739569297,18.45481545784598,0.004371809512384027
+80.7213461835945,-69.44277393265013,19.02744616860421,0.005064548402736313
+80.72825455016927,-69.4459183267922,18.91126897464612,0.00510007995848168
+80.7313996806274,-69.44867713838086,17.897298974101577,0.0031405893954151083
+80.72803699363548,-69.45133844442186,17.99083305552155,0.0034015910366744363
+80.73746269558515,-69.44890937080868,18.617051129540272,0.0043309156723551545
+80.74189341069834,-69.44834497289608,17.990071418249613,0.003403237327372632
diff --git a/notebooks/MIRI/psf_photometry/miri_photometry_space_phot_lvl2.txt b/notebooks/MIRI/psf_photometry/miri_photometry_space_phot_lvl2.txt
new file mode 100644
index 000000000..ef1f585d2
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/miri_photometry_space_phot_lvl2.txt
@@ -0,0 +1,14 @@
+ra,dec,mag,magerr
+80.68750867963841,-69.4452238039411,18.8987486196327,0.007059304642362358
+80.70519397861517,-69.4375332503123,19.40439122349033,0.00574638924316691
+80.69946910770342,-69.44357820826144,18.6802314131745,0.00437528787401566
+80.70077115158013,-69.44323387512023,18.92399381590511,0.0049207578519711095
+80.70479728727508,-69.44154223122871,19.128414182683684,0.0051126641612609935
+80.69800265600595,-69.44754854341006,19.90112453402387,0.010390970769220255
+80.70684753416974,-69.44434611293362,18.446060889979826,0.004176987448988163
+80.72134563364378,-69.44277347750028,19.0083488916964,0.004891763992286455
+80.72823615410552,-69.44592402457953,18.899885332378553,0.004626027400772227
+80.73141574024828,-69.44867558736375,17.86882507199336,0.0031282595211084055
+80.7280218749025,-69.45134171048805,17.957978159475005,0.003345182046217119
+80.73747567689624,-69.4489089318969,18.579415509346774,0.004506085539988072
+80.74188468821127,-69.44834673407533,17.952256398733777,0.0034110607098502845
diff --git a/notebooks/MIRI/psf_photometry/miri_photutils.ipynb b/notebooks/MIRI/psf_photometry/miri_photutils.ipynb
new file mode 100644
index 000000000..1e2565005
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/miri_photutils.ipynb
@@ -0,0 +1,1080 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b58d6954",
+ "metadata": {},
+ "source": [
+ "# MIRI PSF Photometry with Photutils\n",
+ "\n",
+ "**Author**: Ori Fox
\n",
+ "\n",
+ "**Submitted**: November, 2023
\n",
+ "**Updated**: November, 2023
\n",
+ "\n",
+ "**Use case**: PSF Photometry using [Photutils](https://photutils.readthedocs.io/en/stable/). The purpose here is to illustrate the workflow and runtime for using Photutils in a variety of use cases.\n",
+ "\n",
+ "Generally, PSF photometry for data from a space telescope is most accurately performed on pre-mosaiced data. The mosaic process changes the inherent PSF, blurring it both due to resampling and mixing PSFs at different detector positions and rotations. Additionally, accurate theoretical PSF models (e.g., from [WebbPSF](https://webbpsf.readthedocs.io/en/latest/)) are not available for mosaiced data. While an empirical PSF could be constructed (e.g., using Photutils [ePSFBuilder](https://photutils.readthedocs.io/en/latest/epsf.html)) for mosaiced data, the results will generally not be as accurate as performing PSF photometry on the pre-mosaiced data.\n",
+ "\n",
+ "**NOTE:** A companion notebook exists that illustrates how to use perform PSF photometry on both Level 2 and Level 3 data using a new software program called space_phot.
\n",
+ "**Data**: MIRI Data PID 1028 (Calibration Program; Single Star Visit 006 A5V dwarf 2MASSJ17430448+6655015) and MIRI Data PID 1171 (LMC; Multiple Stars).
\n",
+ "**Tools**: photutils, webbpsf, jwst
\n",
+ "**Cross-Instrument**: MIRI
\n",
+ "**Documentation:** This notebook is part of a STScI's larger [post-pipeline Data Analysis Tools Ecosystem](https://jwst-docs.stsci.edu/jwst-post-pipeline-data-analysis) and can be [downloaded](https://github.com/spacetelescope/dat_pyinthesky/tree/main/jdat_notebooks/MRS_Mstar_analysis) directly from the [JDAT Notebook Github directory](https://github.com/spacetelescope/jdat_notebooks).
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "88c61bcf-1c4d-407a-b80c-aa13a01fd746",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Table of contents\n",
+ "1. [Introduction](#intro)
\n",
+ " 1.1 [Python Imports](#imports)
\n",
+ " 1.2 [Set up WebbPSF and Synphot](#setup)
\n",
+ "2. [Download JWST MIRI Data](#data)
\n",
+ "3. [Single Bright Object](#bso)
\n",
+ " 3.1 [Single Level 2 File](#bso2)
\n",
+ " 3.2 [Generate empirical PSF grid for MIRI F770W using WebbPSF](#bso3)
\n",
+ " 3.3 [PSF Photometry](#bso4)
\n",
+ "4. [Faint/Upper Limit, Single Object](#fso)
\n",
+ " 4.1 [Multiple, Level2 Files](#fso2)
\n",
+ "5. [Stellar Field (LMC)](#lmc)
\n",
+ " 5.1 [Multiple Stars, Single Level 2 File](#lmc2)
\n",
+ " 5.2 [Generate empirical PSF grid for MIRI F560W using WebbPSF](#grid2)
\n",
+ " 5.3 [PSF Photometry](#lmc3)
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f572688",
+ "metadata": {},
+ "source": [
+ "# 1. Introduction "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "95891849",
+ "metadata": {},
+ "source": [
+ "**GOALS**:
\n",
+ "\n",
+ "Perform PSF photometry on JWST MIRI images with the [Photutils PSF Photometry tools](https://photutils.readthedocs.io/en/latest/psf.html) using a grid of empirical PSF models from WebbPSF.\n",
+ "\n",
+ "\n",
+ "The notebook shows how to:
\n",
+ "\n",
+ "* generate a [grid of empirical PSF models](https://webbpsf.readthedocs.io/en/latest/psf_grids.html) from WebbPSF
\n",
+ "* perform PSF photometry on the image using the [PSFPhotometry class](https://photutils.readthedocs.io/en/latest/api/photutils.psf.PSFPhotometry.html#photutils.psf.PSFPhotometry)
\n",
+ "\n",
+ "**Data**:
\n",
+ "\n",
+ "MIRI Data PID 1028 (Calibration Program), F770W
\n",
+ "MIRI Data PID 1171 (LMC), F560W/F770W"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5e534877-5c31-4020-9263-4f234f19e1cd",
+ "metadata": {},
+ "source": [
+ "## 1.1 Python Imports "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1ca81097-08d5-470c-942a-d8e7e8fd4479",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import glob\n",
+ "import os\n",
+ "import shutil\n",
+ "import tarfile\n",
+ "from pandas import DataFrame\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "import webbpsf\n",
+ "from urllib.parse import urlparse\n",
+ "import requests\n",
+ "import astropy.units as u\n",
+ "from astropy.coordinates import SkyCoord\n",
+ "from astropy.io import fits\n",
+ "from astropy.nddata import extract_array\n",
+ "from astropy.table import Table\n",
+ "from astropy.visualization import simple_norm\n",
+ "from astroquery.mast import Observations\n",
+ "from jwst.datamodels import ImageModel\n",
+ "from photutils.aperture import CircularAperture\n",
+ "from photutils.background import LocalBackground, MADStdBackgroundRMS, MMMBackground\n",
+ "from photutils.detection import DAOStarFinder\n",
+ "from photutils.psf import GriddedPSFModel, PSFPhotometry"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b762602",
+ "metadata": {},
+ "source": [
+ "## 1.2 Download and Set up Required Data for WebbPSF and Synphot "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c50eace",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Set environmental variables\n",
+ "os.environ[\"WEBBPSF_PATH\"] = \"./webbpsf-data/webbpsf-data\"\n",
+ "os.environ[\"PYSYN_CDBS\"] = \"./grp/redcat/trds/\"\n",
+ "\n",
+ "# required webbpsf data\n",
+ "boxlink = 'https://stsci.box.com/shared/static/qxpiaxsjwo15ml6m4pkhtk36c9jgj70k.gz' \n",
+ "boxfile = './webbpsf-data/webbpsf-data-LATEST.tar.gz'\n",
+ "synphot_url = 'http://ssb.stsci.edu/trds/tarfiles/synphot5.tar.gz'\n",
+ "synphot_file = './synphot5.tar.gz'\n",
+ "\n",
+ "webbpsf_folder = './webbpsf-data'\n",
+ "synphot_folder = './grp'\n",
+ "\n",
+ "\n",
+ "def download_file(url, dest_path, timeout=60):\n",
+ " parsed_url = urlparse(url)\n",
+ " if parsed_url.scheme not in [\"http\", \"https\"]:\n",
+ " raise ValueError(f\"Unsupported URL scheme: {parsed_url.scheme}\")\n",
+ "\n",
+ " response = requests.get(url, stream=True, timeout=timeout)\n",
+ " response.raise_for_status()\n",
+ " with open(dest_path, \"wb\") as f:\n",
+ " for chunk in response.iter_content(chunk_size=8192):\n",
+ " f.write(chunk)\n",
+ "\n",
+ "\n",
+ "# Gather webbpsf files\n",
+ "psfExist = os.path.exists(webbpsf_folder)\n",
+ "if not psfExist:\n",
+ " os.makedirs(webbpsf_folder)\n",
+ " download_file(boxlink, boxfile)\n",
+ " gzf = tarfile.open(boxfile)\n",
+ " gzf.extractall(webbpsf_folder, filter='data')\n",
+ "\n",
+ "# Gather synphot files\n",
+ "synExist = os.path.exists(synphot_folder)\n",
+ "if not synExist:\n",
+ " os.makedirs(synphot_folder)\n",
+ " download_file(synphot_url, synphot_file)\n",
+ " gzf = tarfile.open(synphot_file)\n",
+ " gzf.extractall('./', filter='data')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "68f0b2d7-45a1-4511-858e-51425a50de00",
+ "metadata": {},
+ "source": [
+ "# 2. Download JWST MIRI Data "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6629878-a5d4-4e29-a56e-0f12271016a5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Download Proposal ID 1028 F770W data\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01028/'\n",
+ "\n",
+ "if os.path.isdir(destination_dir):\n",
+ " print(f'Data already downloaded to {os.path.abspath(destination_dir)}')\n",
+ "else:\n",
+ " # Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ " # with proposal ID 1028 and the F770W filter\n",
+ " obs = Observations.query_criteria(proposal_id=1028, filters=['F770W'])\n",
+ " \n",
+ " # Get a list of products associated with the located observation\n",
+ " plist = Observations.get_product_list(obs)\n",
+ " \n",
+ " # Filter the product list to include only specific product subgroups\n",
+ " fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ " \n",
+ " # Download the selected products from the MAST database\n",
+ " Observations.download_products(fplist)\n",
+ " \n",
+ " # Create the destination directory\n",
+ " os.makedirs(destination_dir)\n",
+ " \n",
+ " # Use glob to find all files matching the pattern\n",
+ " files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01028*'))\n",
+ "\n",
+ " # Copy the matching files to the destination directory\n",
+ " for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b649b6f4-aa18-4760-a7d0-979b4e3caec2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Download Proposal ID 1171 F550W and F770W data\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01171/'\n",
+ "\n",
+ "if os.path.isdir(destination_dir):\n",
+ " print(f'Data already downloaded to {os.path.abspath(destination_dir)}')\n",
+ "else:\n",
+ " # Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ " # with proposal ID 1171 and the F550W and F770W filters\n",
+ " obs = Observations.query_criteria(proposal_id=1171, filters=['F560W', 'F770W'])\n",
+ " \n",
+ " # Get a list of products associated with the located observation\n",
+ " plist = Observations.get_product_list(obs)\n",
+ " \n",
+ " # Filter the product list to include only specific product subgroups\n",
+ " fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ " \n",
+ " # Download the selected products from the MAST database\n",
+ " Observations.download_products(fplist)\n",
+ " \n",
+ " # Create the destination directory\n",
+ " os.makedirs(destination_dir)\n",
+ " \n",
+ " # Use glob to find all files matching the pattern\n",
+ " files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01171*'))\n",
+ " \n",
+ " # Copy the matching files to the destination directory\n",
+ " for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5611799d",
+ "metadata": {},
+ "source": [
+ "# 3. Single Bright Star "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6d052f0e-dcb4-4c2a-bc11-4b467dad07c2",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to perform PSF photometry on a single bright star. While aperture photometry is feasible in isolated cases, the user may find PSF photometry preferable in crowded fields or complicated backgrounds."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55c52f95",
+ "metadata": {},
+ "source": [
+ "## 3.1 Single Level 2 File "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "058a14ad-be89-4d7e-934e-1e0a909319c8",
+ "metadata": {},
+ "source": [
+ "In this example, we fit a single, bright source in a single Level 2 images. For a collection of Level 2 images, we could fit each Level 2 image individually and then average the measured fluxes.\n",
+ "\n",
+ "Useful references:
\n",
+ "HST Documentation on PSF Photometry: https://www.stsci.edu/hst/instrumentation/wfc3/data-analysis/psf
\n",
+ "WFPC2 Stellar Photometry with HSTPHOT: https://ui.adsabs.harvard.edu/abs/2000PASP..112.1383D/abstract
\n",
+ "Photutils PSF Fitting Photometry: https://photutils.readthedocs.io/en/stable/psf.html"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2af76a35-14ee-44b2-adb5-d92e66ddfafc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# get the level 2 filenames\n",
+ "path = \"./mast/01028/\"\n",
+ "level2 = sorted(glob.glob(os.path.join(path, '*cal.fits')))\n",
+ "level2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab0ac799-a832-40af-9cd4-b5b3934cfee4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# display the first level-2 image\n",
+ "data = fits.getdata(level2[0])\n",
+ "norm = simple_norm(data, 'sqrt', percent=99)\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(20, 12))\n",
+ "im = ax.imshow(data, origin='lower', norm=norm, cmap='gray')\n",
+ "clb = plt.colorbar(im, label='MJy/sr')\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ab947da-be0a-4cde-88eb-2d947adf2b81",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NaN.\n",
+ "# Here, we'll overwrite the original CAL file.\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "filename = level2[0]\n",
+ "with ImageModel(filename) as model:\n",
+ " model.data[model.dq >= 10] = np.nan\n",
+ " model.save(filename)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "87673dd8-1bc6-4663-bebc-8e2434974ceb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Re-display the image\n",
+ "data = fits.getdata(level2[0])\n",
+ "norm = simple_norm(data, 'sqrt', percent=99)\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(20, 12))\n",
+ "im = ax.imshow(data, origin='lower', norm=norm, cmap='gray')\n",
+ "clb = plt.colorbar(im, label='MJy/sr')\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2f138285-bfc5-4f20-854b-fe5d4530e99b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source. In this case, our source is from MIRI Program ID #1028, a Calibration Program.\n",
+ "# We are using Visit 006, which targets the A5V dwarf 2MASSJ17430448+6655015\n",
+ "# Reference Link: http://simbad.cds.unistra.fr/simbad/sim-basic?Ident=2MASSJ17430448%2B6655015&submit=SIMBAD+search\n",
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "with ImageModel(filename) as model:\n",
+ " x, y = model.meta.wcs.world_to_pixel(source_location)\n",
+ "\n",
+ "cutout = extract_array(data, (21, 21), (y, x))\n",
+ "\n",
+ "fig, ax = plt.subplots(figsize=(8, 8))\n",
+ "norm2 = simple_norm(cutout, 'log', percent=99)\n",
+ "im = ax.imshow(cutout, origin='lower', norm=norm2, cmap='gray')\n",
+ "clb = plt.colorbar(im, label='MJy/sr', shrink=0.8)\n",
+ "ax.set_title('PID1028, Obs006')\n",
+ "\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b24288b6-c6c7-433f-866f-126eea4a9ab3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2024-02-10T00:33:17.900216Z",
+ "iopub.status.busy": "2024-02-10T00:33:17.899831Z",
+ "iopub.status.idle": "2024-02-10T00:33:17.903626Z",
+ "shell.execute_reply": "2024-02-10T00:33:17.902732Z",
+ "shell.execute_reply.started": "2024-02-10T00:33:17.900187Z"
+ }
+ },
+ "source": [
+ "## 3.2 Generate empirical PSF grid for MIRI F770W using WebbPSF "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f7472c4e-f418-4f39-b8b6-5d824c1c4c65",
+ "metadata": {},
+ "source": [
+ "Let's now use WebbPSF to generate an empirical grid of ePSF models for MIRI F770W.\n",
+ "The output will be a Photutils [GriddedPSFModel](https://photutils.readthedocs.io/en/latest/api/photutils.psf.GriddedPSFModel.html#photutils.psf.GriddedPSFModel) containing a 2x2 grid of detector-position-dependent empirical PSFs, each oversampled by a factor of 4. Note that we save the PSF grid to a FITS file (via `save=True`) called `miri_mirim_f770w_fovp101_samp4_npsf4.fits`. To save time in future runs, we load this FITS file directly into a `GriddedPSFModel` object:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "19b97fc8-35b4-460f-b154-72bdd4c45f66",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psfgrid_filename = 'miri_mirim_f770w_fovp101_samp4_npsf4.fits'\n",
+ "\n",
+ "if not os.path.exists(psfgrid_filename):\n",
+ " miri = webbpsf.MIRI()\n",
+ " miri.filter = 'F770W'\n",
+ " psf_model = miri.psf_grid(num_psfs=4, all_detectors=True, verbose=True, save=True)\n",
+ "else:\n",
+ " psf_model = GriddedPSFModel.read(psfgrid_filename)\n",
+ "\n",
+ "psf_model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7d1640dc-6c64-4fe7-a208-ddbcdc529512",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# display the PSF grid\n",
+ "psf_model.plot_grid()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7294300-6ba8-418b-a61c-4194af270de3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# display the PSF grid deltas from the mean ePSF\n",
+ "psf_model.plot_grid(deltas=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bcab882b-16f6-4aef-9f4f-b3af8b3b1b14",
+ "metadata": {},
+ "source": [
+ "## 3.3 PSF Photometry "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b34c6e74-1a52-4874-afc1-5a444662e966",
+ "metadata": {},
+ "source": [
+ "Now let's use our gridded PSF model to perform PSF photometry."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f42d294f-a5db-4a2a-be4a-e40a26374207",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# load data and convert units from MJy/sr to uJy\n",
+ "with ImageModel(filename) as model:\n",
+ " unit = u.Unit(model.meta.bunit_data)\n",
+ " data = model.data << unit\n",
+ " error = model.err << unit\n",
+ "\n",
+ " # use pixel area map because of geometric distortion in level-2 data \n",
+ " pixel_area = model.area * model.meta.photometry.pixelarea_steradians * u.sr\n",
+ " data *= pixel_area\n",
+ " error *= pixel_area\n",
+ " \n",
+ " data = data.to(u.uJy)\n",
+ " error = error.to(u.uJy)\n",
+ "\n",
+ "data.unit, error.unit"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "318ab078-ec51-4b71-9641-dd1fe566138a",
+ "metadata": {},
+ "source": [
+ "To perform photometry on a single source we can input a Table containing its (x, y) position."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6efc06f3-445d-4139-a404-fff43ad15804",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "init_params = Table()\n",
+ "init_params['x'] = [x]\n",
+ "init_params['y'] = [y]\n",
+ "init_params"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "61424bfb-7c76-489b-9ccf-efc9c324472d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# we turn off the finder because we input the source position\n",
+ "fit_shape = 5\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "psfphot = PSFPhotometry(psf_model, fit_shape, finder=None, aperture_radius=fit_shape, \n",
+ " localbkg_estimator=localbkg_estimator, progress_bar=True)\n",
+ "phot = psfphot(data, error=error, init_params=init_params)\n",
+ "phot"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "62945e0b-6eb7-487f-a7b6-8e3c718b3c26",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# convert fit flux from uJy to ABmag\n",
+ "flux = phot['flux_fit']\n",
+ "flux_err = phot['flux_err']\n",
+ "mag = phot['flux_fit'].to(u.ABmag)\n",
+ "magerr = 2.5 * np.log10(1.0 + (flux_err / flux))\n",
+ "magerr = magerr.value * u.ABmag\n",
+ "mag, magerr"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6c8baf43-51bc-4ff5-97e9-10db55ab6128",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig, ax = plt.subplots(ncols=3, figsize=(12, 4))\n",
+ "\n",
+ "shape = (21, 21)\n",
+ "cutout1 = extract_array(data.value, shape, (y, x))\n",
+ "norm = simple_norm(cutout1, 'log', percent=98)\n",
+ "im1 = ax[0].imshow(cutout1, origin='lower', norm=norm)\n",
+ "ax[0].set_title(r'Data ($\\mu$Jy)')\n",
+ "plt.colorbar(im1, shrink=0.7)\n",
+ "\n",
+ "model = psfphot.make_model_image(data.shape, shape)\n",
+ "cutout2 = extract_array(model, shape, (y, x))\n",
+ "im2 = ax[1].imshow(cutout2, origin='lower', norm=norm)\n",
+ "ax[1].set_title('Fit PSF Model')\n",
+ "plt.colorbar(im2, shrink=0.7)\n",
+ "\n",
+ "resid = psfphot.make_residual_image(data.value, shape)\n",
+ "cutout3 = extract_array(resid, shape, (y, x))\n",
+ "norm3 = simple_norm(cutout3, 'sqrt', percent=99)\n",
+ "im3 = ax[2].imshow(cutout3, origin='lower', norm=norm3)\n",
+ "ax[2].set_title('Residual')\n",
+ "plt.colorbar(im3, shrink=0.7)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b5f0ad5-b59e-4eff-8687-f6a2199d8bd9",
+ "metadata": {},
+ "source": [
+ "# 4. Faint/Upper Limit, Single Object "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1dc60da1-0f4d-4e5e-a109-f7352dfd0fdc",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to calculate an upper limit at a fixed (x, y) position using forced PSF photometry a blank part of the sky.\n",
+ "\n",
+ "We'll use the same data as Section 3."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9311116e-630b-4a2e-8fc6-be75f6de61fd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# load data and convert units from MJy/sr to uJy\n",
+ "with ImageModel(filename) as model:\n",
+ " unit = u.Unit(model.meta.bunit_data)\n",
+ " data = model.data << unit\n",
+ " error = model.err << unit\n",
+ " \n",
+ " pixel_area = pixel_area = model.meta.photometry.pixelarea_steradians * u.sr\n",
+ " data *= pixel_area\n",
+ " error *= pixel_area\n",
+ " \n",
+ " data = data.to(u.uJy)\n",
+ " error = error.to(u.uJy)\n",
+ "\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "with ImageModel(filename) as model:\n",
+ " x, y = model.meta.wcs.world_to_pixel(source_location)\n",
+ "\n",
+ "cutout = extract_array(data.value, (21, 21), (y, x))\n",
+ "\n",
+ "fig, ax = plt.subplots()\n",
+ "norm = simple_norm(cutout, 'sqrt', percent=95)\n",
+ "im = ax.imshow(cutout, origin='lower', norm=norm, cmap='gray')\n",
+ "clb = plt.colorbar(im, label=r'$\\mu$Jy')\n",
+ "ax.set_title('PID1028, Obs006')\n",
+ "\n",
+ "ax.set_xlabel('Pixels')\n",
+ "ax.set_ylabel('Pixels')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "63ac28fd-ad3b-479b-8561-ca5913925aba",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# to perform forced photometry, we set the (x, y) source position\n",
+ "# AND we fix the PSF model position so that it does not vary in the fit\n",
+ "# (only flux will be fit)\n",
+ "init_params = Table()\n",
+ "init_params['x'] = [x]\n",
+ "init_params['y'] = [y]\n",
+ "\n",
+ "# This requires photutils 1.11.0\n",
+ "psf_model_forced = psf_model.copy()\n",
+ "psf_model_forced.x_0.fixed = True\n",
+ "psf_model_forced.y_0.fixed = True\n",
+ "psf_model_forced.fixed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a13930cb-fd2a-447e-a68f-357eb7014ad3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fit_shape = 5\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "psfphot = PSFPhotometry(psf_model_forced, fit_shape, finder=None, aperture_radius=fit_shape, \n",
+ " localbkg_estimator=localbkg_estimator, progress_bar=True)\n",
+ "\n",
+ "phot = psfphot(data, error=error, init_params=init_params)\n",
+ "phot"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3206847e-2e2e-4bc3-a51e-3e5877f87b28",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# To calculate upper limit, multiply the flux_err by your desired sigma\n",
+ "sigma = 3.0\n",
+ "limit = sigma * phot['flux_err']\n",
+ "limit.to(u.ABmag)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e4996d2-6274-473d-b13c-f3848c27ad78",
+ "metadata": {},
+ "source": [
+ "## Note: you can go significantly deeper with the Level 3 combined data product"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a969717-bbef-40b9-ac9b-f83dec99dc09",
+ "metadata": {},
+ "source": [
+ "# 5. Stellar Field (LMC) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "da877310-fd47-41d6-afea-7fa725a546af",
+ "metadata": {},
+ "source": [
+ "In this case, we are going to do the same steps as in Section 3, but for multiple stars. The purpose is to illustrate the workflow and runtime for using Photutils on a large number of stars."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32bdafe6-db19-4080-9587-b9785c2f7fa7",
+ "metadata": {},
+ "source": [
+ "## 5.1 Multiple Stars, Single Level 2 File "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "72aabedd-8d80-4c6e-8d3d-9287577665bf",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Find stars in Level 3 File\n",
+ "path = './mast/01171/'\n",
+ "level3 = os.path.join(path, 'jw01171-o004_t001_miri_f560w_i2d.fits')\n",
+ "level3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4eb6155b-5f1a-4f92-9c34-5cd0876e439d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get rough estimate of background (there are better ways to do background subtraction)\n",
+ "bkgrms = MADStdBackgroundRMS()\n",
+ "mmm_bkg = MMMBackground()\n",
+ "\n",
+ "with ImageModel(level3) as model:\n",
+ " wcs_l3 = model.meta.wcs\n",
+ " std = bkgrms(model.data)\n",
+ " bkg = mmm_bkg(model.data)\n",
+ " data_bkgsub = model.data.copy()\n",
+ " data_bkgsub -= bkg \n",
+ "\n",
+ "# Find stars\n",
+ "# F560W FWHM = 1.882 pix\n",
+ "fwhm_psf = 1.882\n",
+ "threshold = 5.0\n",
+ "daofind = DAOStarFinder(threshold=threshold * std, fwhm=fwhm_psf, exclude_border=True, min_separation=10)\n",
+ "found_stars = daofind(data_bkgsub)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "59337919-ee59-4f09-a716-1f978ab5c775",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "found_stars.pprint_all(max_lines=10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "73aba802",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# plot the found stars\n",
+ "norm = simple_norm(data_bkgsub, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data_bkgsub, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(found_stars['xcentroid'], found_stars['ycentroid'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "84411432-3270-47de-a546-a81935c19c5c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig, ax = plt.subplots(nrows=2, figsize=(10, 8))\n",
+ "\n",
+ "ax[0].scatter(found_stars['mag'], found_stars['sharpness'], s=10, color='k')\n",
+ "ax[0].set_xlabel('mag')\n",
+ "ax[0].set_ylabel('sharpness')\n",
+ "\n",
+ "ax[1].scatter(found_stars['mag'], found_stars['roundness2'], s=10, color='k')\n",
+ "ax[1].set_xlabel('mag')\n",
+ "ax[1].set_ylabel('roundness')\n",
+ "\n",
+ "mag0 = -3.0\n",
+ "mag1 = -5.0\n",
+ "for ax_ in ax:\n",
+ " ax_.axvline(mag0, color='red', linestyle='dashed')\n",
+ " ax_.axvline(mag1, color='red', linestyle='dashed')\n",
+ "\n",
+ "sh0 = 0.40\n",
+ "sh1 = 0.82\n",
+ "ax[0].axhline(sh0, color='red', linestyle='dashed')\n",
+ "ax[0].axhline(sh1, color='red', linestyle='dashed')\n",
+ "\n",
+ "rnd0 = -0.40\n",
+ "rnd1 = 0.40\n",
+ "ax[1].axhline(rnd0, color='red', linestyle='dashed')\n",
+ "ax[1].axhline(rnd1, color='red', linestyle='dashed')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "04f2dbae-1b1c-4f67-902f-eb91f73b2d6d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mask = ((found_stars['mag'] < mag0) & (found_stars['mag'] > mag1) & (found_stars['roundness2'] > rnd0)\n",
+ " & (found_stars['roundness2'] < rnd1) & (found_stars['sharpness'] > sh0) \n",
+ " & (found_stars['sharpness'] < sh1) & (found_stars['xcentroid'] > 100) & (found_stars['xcentroid'] < 700)\n",
+ " & (found_stars['ycentroid'] > 100) & (found_stars['ycentroid'] < 700))\n",
+ "\n",
+ "found_stars_sel = found_stars[mask]\n",
+ "\n",
+ "print('Number of stars found originally:', len(found_stars))\n",
+ "print('Number of stars in final selection:', len(found_stars_sel))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "948f0a96-8bdc-4c94-95db-8edf513e4094",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot the selected stars\n",
+ "norm = simple_norm(data_bkgsub, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data_bkgsub, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(found_stars_sel['xcentroid'], found_stars_sel['ycentroid'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a221246a-9acb-4c79-9831-2f6cb72bb90a",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2024-02-10T02:52:51.761591Z",
+ "iopub.status.busy": "2024-02-10T02:52:51.761230Z",
+ "iopub.status.idle": "2024-02-10T02:52:51.765256Z",
+ "shell.execute_reply": "2024-02-10T02:52:51.764441Z",
+ "shell.execute_reply.started": "2024-02-10T02:52:51.761562Z"
+ }
+ },
+ "source": [
+ "## 5.2 Generate empirical PSF grid for MIRI F560W using WebbPSF "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b5fdcf15-7f72-40b8-b1aa-2b5fb0375ed8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psfgrid_filename = 'miri_mirim_f560w_fovp101_samp4_npsf4.fits'\n",
+ "\n",
+ "if not os.path.exists(psfgrid_filename):\n",
+ " miri = webbpsf.MIRI()\n",
+ " miri.filter = 'F560W'\n",
+ " psf_model = miri.psf_grid(num_psfs=4, all_detectors=True, verbose=True, save=True)\n",
+ "else:\n",
+ " psf_model = GriddedPSFModel.read(psfgrid_filename)\n",
+ "\n",
+ "psf_model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29fa22f3-0331-4966-9bb4-c75f325011c8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psf_model.plot_grid()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b2350526-2bba-4322-9092-88b6acdc117b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# get the level 2 image\n",
+ "# here, we'll use the PID 1171 files\n",
+ "path = \"./mast/01171/\"\n",
+ "level2 = sorted(glob.glob(os.path.join(path, 'jw01171004*cal.fits')))\n",
+ "filename = level2[0]\n",
+ "print(filename)\n",
+ "\n",
+ "# load data and convert units from MJy/sr to uJy\n",
+ "with ImageModel(filename) as model:\n",
+ " unit = u.Unit(model.meta.bunit_data)\n",
+ " model.data[model.dq >= 10] = np.nan\n",
+ " data = model.data << unit\n",
+ " error = model.err << unit\n",
+ " \n",
+ " pixel_area = pixel_area = model.meta.photometry.pixelarea_steradians * u.sr\n",
+ " data *= pixel_area\n",
+ " error *= pixel_area\n",
+ " \n",
+ " data = data.to(u.uJy)\n",
+ " error = error.to(u.uJy)\n",
+ "\n",
+ " wcs = model.meta.wcs\n",
+ "\n",
+ "data.unit, error.unit"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9aa77cc2-26e2-4ac3-8384-90b9f8567f22",
+ "metadata": {},
+ "source": [
+ "## 5.3 PSF Photometry "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3b416d45-1081-4a16-b189-cc0e5e9d4e4e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# translate (x, y) positions from the level 3 image to the level 2 image\n",
+ "xc = found_stars_sel['xcentroid']\n",
+ "yc = found_stars_sel['ycentroid']\n",
+ "sc = wcs_l3.pixel_to_world(xc, yc)\n",
+ "\n",
+ "x, y = wcs.world_to_pixel(sc)\n",
+ "init_params = Table()\n",
+ "init_params['x'] = x\n",
+ "init_params['y'] = y\n",
+ "\n",
+ "# we need to remove stars in the masked region of\n",
+ "# the level-2 data\n",
+ "mask = x > 400\n",
+ "init_params = init_params[mask]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f17e393a-9f0b-4b83-83b9-4bb8497dd835",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "mask"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "511183e0-0adc-4966-b7f4-fcd912b054e9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot the selected stars\n",
+ "norm = simple_norm(data.value, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data.value, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(init_params['x'], init_params['y'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "84812421-f447-4b75-9fe4-e377fba54b17",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fit_shape = 5\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "psfphot = PSFPhotometry(psf_model, fit_shape, finder=None, aperture_radius=fit_shape, \n",
+ " localbkg_estimator=localbkg_estimator, progress_bar=True)\n",
+ "phot = psfphot(data, error=error, init_params=init_params)\n",
+ "phot"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8d5899cf-8956-4b3c-941a-67b0fedc0c51",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# convert fit flux from uJy to ABmag\n",
+ "flux = phot['flux_fit']\n",
+ "flux_err = phot['flux_err']\n",
+ "mag = phot['flux_fit'].to(u.ABmag)\n",
+ "magerr = 2.5 * np.log10(1.0 + (flux_err / flux))\n",
+ "magerr = magerr.value * u.ABmag\n",
+ "mag, magerr"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a5ca496b-9826-450e-982c-f671c009c23f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Write to File\n",
+ "df = DataFrame({\"RA\": sc.ra.deg[mask], \"DEC\": sc.dec.deg[mask], \"Mag\": mag.value, \"Mag Err\": magerr.value})\n",
+ "df.to_csv('miri_photometry_photutils.txt', index=False) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5630029f-31d1-42cd-8454-225e86cabc48",
+ "metadata": {},
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "843b5201-6f57-46f0-9da0-b738714178d3",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ },
+ "toc-showcode": false
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/MIRI/psf_photometry/miri_spacephot.ipynb b/notebooks/MIRI/psf_photometry/miri_spacephot.ipynb
new file mode 100644
index 000000000..c286c8b1d
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/miri_spacephot.ipynb
@@ -0,0 +1,1981 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b58d6954",
+ "metadata": {},
+ "source": [
+ "# MIRI PSF Photometry With Space_Phot\n",
+ "\n",
+ "**Author**: Ori Fox
\n",
+ "\n",
+ "**Submitted**: August, 2023
\n",
+ "**Updated**: November, 2023
\n",
+ "\n",
+ "**Use case**: PSF Photometry on Level3 data using dedicated package Space_Phot (https://github.com/jpierel14/space_phot). Space_phot is built on Astropy's Photutils package. Unlike photutils, space_phot can be used on Level3 data. This is because has built in functionality that can generate a resampled Level3 PSF at a given detector position. Such use cases are particularly useful for faint, point source targets or deep upper limits where observers need to take advantage of the combined image stack. For a large number of bright sources, users may find space_phot to be too slow and should consider other packages, such as DOLPHOT and/or Photutils. **NOTE:** A companion notebook exists that illustrates how to use Photutils for the same Level2 data set.
\n",
+ "**Important Note**: When not to use. Due to the sensitivity of the space_phot parameters, this tool is not meant to be used for a large sample of stars (i.e., Section 5 below). If a user would like to use space_phot on more than one source, they should carefully construct a table of parameters that are carefully refined for each source.\n",
+ "**Data**: MIRI Data PID 1028 (Calibration Program; Single Star Visit 006 A5V dwarf 2MASSJ17430448+6655015) and MIRI Data PID 1171 (LMC; Multiple Stars).
\n",
+ "**Tools**: photutils, space_phot drizzlepac, jupyter
\n",
+ "**Cross-Instrument**: NIRCam, MIRI.
\n",
+ "**Documentation**: This notebook is part of a STScI's larger post-pipeline Data Analysis Tools Ecosystem and can be downloaded directly from the JDAT Notebook Github directory.
\n",
+ "**Pipeline Version**: JWST Pipeline
\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "88c61bcf-1c4d-407a-b80c-aa13a01fd746",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Table of contents\n",
+ "1. [Introduction](#intro)
\n",
+ " 1.1 [Setup](#webbpsf)
\n",
+ " 1.2 [Python imports](#py_imports)
\n",
+ "2. [Download Data](#data)
\n",
+ "3. [Bright, Single Object](#bso)
\n",
+ " 3.1 [Multiple, Level2 Files](#bso2)
\n",
+ " 3.2 [Single, Level3 Mosaicked File](#bso3)
\n",
+ "4. [Faint/Upper Limit, Single Object](#fso)
\n",
+ " 4.1 [Multiple, Level2 Files](#fso2)
\n",
+ " 4.2 [Single, Level3 Mosaicked File](#fso3)
\n",
+ "5. [Stellar Field (LMC)](#lmv)
\n",
+ " 5.1 [Multiple, Level2 Files](#lmc2)
\n",
+ " 5.2 [Single, Level3 Mosaicked File](#lmc3)
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f572688",
+ "metadata": {},
+ "source": [
+ "1.-Introduction \n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "95891849",
+ "metadata": {},
+ "source": [
+ "GOALS:
\n",
+ "\n",
+ "PSF Photometry can be obtained using:
\n",
+ "\n",
+ "* grid of PSF models from WebbPSF
\n",
+ "* single effective PSF (ePSF) NOT YET AVAILABLE
\n",
+ "* grid of effective PSF NOT YET AVAILABLE
\n",
+ "\n",
+ "The notebook shows:
\n",
+ "\n",
+ "* how to obtain the PSF model from WebbPSF (or build an ePSF)
\n",
+ "* how to perform PSF photometry on the image
\n",
+ "\n",
+ "**Data**:
\n",
+ "\n",
+ "MIRI Data PID 1028 (Calibration Program), F770W
\n",
+ "MIRI Data PID 1171 (LMC), F560W/F770W"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b762602",
+ "metadata": {},
+ "source": [
+ "### 1.1-Setup WebbPSF and Synphot Directories ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6b5fc65d-f570-4e9c-a0ca-bf460a5e2616",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import space_phot\n",
+ "from importlib.metadata import version\n",
+ "version('space_phot')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c50eace",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import glob\n",
+ "import shutil\n",
+ "import tarfile\n",
+ "import requests\n",
+ "from urllib.parse import urlparse\n",
+ "\n",
+ "# Set environmental variables\n",
+ "os.environ[\"WEBBPSF_PATH\"] = \"./webbpsf-data/webbpsf-data\"\n",
+ "os.environ[\"PYSYN_CDBS\"] = \"./grp/redcat/trds/\"\n",
+ "\n",
+ "# WEBBPSF Data\n",
+ "boxlink = 'https://stsci.box.com/shared/static/qxpiaxsjwo15ml6m4pkhtk36c9jgj70k.gz' \n",
+ "boxfile = './webbpsf-data/webbpsf-data-1.0.0.tar.gz'\n",
+ "synphot_url = 'http://ssb.stsci.edu/trds/tarfiles/synphot5.tar.gz'\n",
+ "synphot_file = './synphot5.tar.gz'\n",
+ "\n",
+ "webbpsf_folder = './webbpsf-data'\n",
+ "synphot_folder = './grp'\n",
+ "\n",
+ "\n",
+ "def download_file(url, dest_path, timeout=60):\n",
+ " parsed_url = urlparse(url)\n",
+ " if parsed_url.scheme not in [\"http\", \"https\"]:\n",
+ " raise ValueError(f\"Unsupported URL scheme: {parsed_url.scheme}\")\n",
+ "\n",
+ " response = requests.get(url, stream=True, timeout=timeout)\n",
+ " response.raise_for_status()\n",
+ " with open(dest_path, \"wb\") as f:\n",
+ " for chunk in response.iter_content(chunk_size=8192):\n",
+ " f.write(chunk)\n",
+ "\n",
+ "\n",
+ "# Gather webbpsf files\n",
+ "psfExist = os.path.exists(webbpsf_folder)\n",
+ "if not psfExist:\n",
+ " os.makedirs(webbpsf_folder)\n",
+ " download_file(boxlink, boxfile)\n",
+ " gzf = tarfile.open(boxfile)\n",
+ " gzf.extractall(webbpsf_folder, filter='data')\n",
+ "\n",
+ "# Gather synphot files\n",
+ "synExist = os.path.exists(synphot_folder)\n",
+ "if not synExist:\n",
+ " os.makedirs(synphot_folder)\n",
+ " download_file(synphot_url, synphot_file)\n",
+ " gzf = tarfile.open(synphot_file)\n",
+ " gzf.extractall('./', filter='data')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5e534877-5c31-4020-9263-4f234f19e1cd",
+ "metadata": {},
+ "source": [
+ "### 1.2-Python Imports ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "93e91ff7-ea71-4507-b38d-c2067cae3a8f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from astropy.io import fits\n",
+ "from astropy.table import QTable\n",
+ "from astropy.nddata import extract_array\n",
+ "from astropy.coordinates import SkyCoord\n",
+ "from astropy import wcs\n",
+ "from astropy.wcs.utils import skycoord_to_pixel\n",
+ "from astropy import units as u\n",
+ "from astropy.visualization import simple_norm\n",
+ "from astroquery.mast import Observations\n",
+ "from importlib.metadata import version\n",
+ "import time\n",
+ "import math\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "import pandas as pd\n",
+ "import warnings\n",
+ "%matplotlib inline\n",
+ "\n",
+ "# JWST models\n",
+ "from jwst.datamodels import ImageModel\n",
+ "\n",
+ "# Background and PSF Functions\n",
+ "from photutils.background import MMMBackground, MADStdBackgroundRMS, LocalBackground\n",
+ "\n",
+ "# Photutils library and tools\n",
+ "from photutils.aperture import CircularAperture\n",
+ "from photutils.detection import DAOStarFinder\n",
+ "\n",
+ "# set up crds if necessary\n",
+ "from crds import client\n",
+ "if os.environ.get(\"CRDS_PATH\") is None:\n",
+ " client.set_crds_server('https://jwst-crds.stsci.edu')\n",
+ " os.environ[\"CRDS_SERVER_URL\"] = \"https://jwst-crds.stsci.edu\"\n",
+ " os.environ[\"CRDS_PATH\"] = \"\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "68f0b2d7-45a1-4511-858e-51425a50de00",
+ "metadata": {},
+ "source": [
+ "2.-Download Data\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6629878-a5d4-4e29-a56e-0f12271016a5",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "# Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ "# with proposal ID 1028 and a specific filter 'F770W'\n",
+ "obs = Observations.query_criteria(proposal_id=1028, filters=['F770W'])\n",
+ "\n",
+ "# Get a list of products associated with the located observation\n",
+ "plist = Observations.get_product_list(obs)\n",
+ "\n",
+ "# Filter the product list to include only specific product subgroups: 'RATE', 'CAL', 'I2D', and 'ASN'\n",
+ "fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ "\n",
+ "# Download the selected products from the MAST database (UNCOMMENT TO DOWNLOAD)\n",
+ "Observations.download_products(fplist)\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01028/'\n",
+ "\n",
+ "# Create the destination directory if it doesn't exist\n",
+ "if not os.path.exists(destination_dir):\n",
+ " os.makedirs(destination_dir)\n",
+ "\n",
+ "# Use glob to find all files matching the pattern 'mastDownload/JWST/j*/jw01537*cal.fits'\n",
+ "files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01028*'))\n",
+ "\n",
+ "# Copy the matching files to the destination directory\n",
+ "for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b649b6f4-aa18-4760-a7d0-979b4e3caec2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Query the MAST (Mikulski Archive for Space Telescopes) database for observations\n",
+ "# with proposal ID 1171 and a specific filters 'F550W' and 'F770W'\n",
+ "obs = Observations.query_criteria(proposal_id=1171, filters=['F560W', 'F770W'])\n",
+ "\n",
+ "# Get a list of products associated with the located observation\n",
+ "plist = Observations.get_product_list(obs)\n",
+ "\n",
+ "# Filter the product list to include only specific product subgroups: 'RATE', 'CAL', 'I2D', and 'ASN'\n",
+ "fplist = Observations.filter_products(plist, productSubGroupDescription=['CAL', 'I2D', 'ASN'])\n",
+ "\n",
+ "# Download the selected products from the MAST database (UNCOMMENT TO DOWNLOAD)\n",
+ "Observations.download_products(fplist)\n",
+ "\n",
+ "# Define source and destination directories\n",
+ "source_dir = 'mastDownload/JWST/'\n",
+ "destination_dir = 'mast/01171/'\n",
+ "\n",
+ "# Create the destination directory if it doesn't exist\n",
+ "if not os.path.exists(destination_dir):\n",
+ " os.makedirs(destination_dir)\n",
+ "\n",
+ "# Use glob to find all files matching the pattern 'mastDownload/JWST/j*/jw01537*cal.fits'\n",
+ "files_to_copy = glob.glob(os.path.join(source_dir, 'j*/jw01171*'))\n",
+ "\n",
+ "# Copy the matching files to the destination directory\n",
+ "for file_path in files_to_copy:\n",
+ " shutil.copy(file_path, destination_dir)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5611799d",
+ "metadata": {},
+ "source": [
+ "3.-Bright, Single Object\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6d052f0e-dcb4-4c2a-bc11-4b467dad07c2",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to perform PSF photometry on a single, bright object. While aperture photometry is feasible in isolated cases, the user may find PSF photometry preferable in crowded fields or complicated backgrounds."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55c52f95",
+ "metadata": {},
+ "source": [
+ "### 3.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "058a14ad-be89-4d7e-934e-1e0a909319c8",
+ "metadata": {},
+ "source": [
+ "Generally, PSF photometry for data from a space telescope is most accurately performed on pre-mosaiced data. In the case of HST, that corresponds to FLT files rather than DRZ. And in the case of JWST, this corresponds to Level2 files rather than Level3. The reason is that a mosaiced PSF changes the inherent PSF as a function of position on the detector so that there is no adequate model (theoretical or empirical) to use.
\n",
+ "\n",
+ "In this example, we aim to fit a source simultaneously across multiple Level 2 images. A more basic approach would be to fit each Level 2 file individually and then average together the measured fluxes. However, this approach more easily corrects for bad pixels or cosmic rays that are only in one image and allows for a more accurate photometric solution by reducing the number of free parameters per source.
\n",
+ "\n",
+ "Useful references:
\n",
+ "HST Documentation on PSF Photometry: https://www.stsci.edu/hst/instrumentation/wfc3/data-analysis/psf
\n",
+ "WFPC2 Stellar Photometry with HSTPHOT: https://ui.adsabs.harvard.edu/abs/2000PASP..112.1383D/abstract
\n",
+ "Space-Phot Documentation on Level2 Fitting: https://space-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8f44a6cf",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Define Level 3 File\n",
+ "lvl3 = ['./mast/01028/jw01028-o006_t001_miri_f770w_i2d.fits']\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d35e67aa",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create Level 2 Data List from ASN files\n",
+ "prefix = \"./mast/01028/\"\n",
+ "asn = glob.glob(prefix+'jw01028-o006_*_image3_00004_asn.json')\n",
+ "\n",
+ "with open(asn[0], \"r\") as fi:\n",
+ " lvl2 = []\n",
+ " for ln in fi:\n",
+ " #print(ln)\n",
+ " if ln.startswith(' \"expname\":'):\n",
+ " x = ln[2:].split(':')\n",
+ " y = x[1].split('\"')\n",
+ " lvl2.append(prefix+y[1])\n",
+ "print(lvl2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab0ac799-a832-40af-9cd4-b5b3934cfee4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (Before DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "06d8c388-bd4a-4f5b-809e-a3e9dc10940c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (Before DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ab947da-be0a-4cde-88eb-2d947adf2b81",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "for file in lvl2:\n",
+ " ref_fits = ImageModel(file)\n",
+ " data = ref_fits.data\n",
+ " dq = ref_fits.dq\n",
+ " data[dq >= 10] = np.nan\n",
+ " ref_fits.data = data\n",
+ " ref_fits.save(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2f92835e-dd6b-43fe-b21a-b7ab4aa0ea62",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "for file in lvl2:\n",
+ " hdul = fits.open(file, mode='update')\n",
+ " data = fits.open(file)['SCI', 1].data\n",
+ " dq = fits.open(file)['DQ', 1].data\n",
+ " data[dq >= 10] = np.nan\n",
+ " hdul['SCI', 1].data = data\n",
+ " hdul.flush()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "87673dd8-1bc6-4663-bebc-8e2434974ceb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (After DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4858f4d9-dbc9-40f1-a00c-80339cc69fae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source. In this case, our source is from MIRI Program ID #1028, a Calibration Program.\n",
+ "# We are using Visit 006, which targets the A5V dwarf 2MASSJ17430448+6655015\n",
+ "# Reference Link: http://simbad.cds.unistra.fr/simbad/sim-basic?Ident=2MASSJ17430448%2B6655015&submit=SIMBAD+search\n",
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.3, max_cut=15)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "411f9ecd-2e6b-4de6-a452-1b633dee7e57",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (After DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a77acdfe-b659-443e-8ff7-afc5a24f2cdb",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source\n",
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d67d57b9",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get the PSF from WebbPSF using defaults.\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "psfs = space_phot.get_jwst_psf(jwst_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cee4ba3b-2a71-4685-ae1f-5beb60c7d4ad",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psfs[0].data, (41, 41), (122, 122))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.2)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bf53473e-9671-4f80-9d59-29c4ed8bab09",
+ "metadata": {},
+ "source": [
+ "#### Notes on the PSF Fitting in Space_Phot:
\n",
+ "\n",
+ "https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "As noted above, improved documentation will be coming. For now, here are some important points to consider.\n",
+ "\n",
+ "All fitting is performed with Astropy's Photutils. As with any photometry program, the printed statistical errors are good indicators of your success.\n",
+ "\n",
+ "There are different fitting techniques, but when the fit_flux parameter is set to 'single', the source is fit simultaneously in all Level2 images. There is good reason for this outlined in a paper for PSF fitting in Hubble: https://iopscience.iop.org/article/10.1086/316630/pdf\n",
+ "\n",
+ "As a result, the flux and corresponding error take into account a single overall fit. As part of this, the fitting therefore assumes a constant zero point across all images. While this is not exactly true, it is typically true to within 1\\% and good enough for our purposes. Users can alternatively fit with the fit_flux parameter set to 'multi', which treats each image independently. The final flux must therefore be averaged.\n",
+ "\n",
+ "When you run space_phot, you will see some additional diagnositics displayed in the cell. At the top, the % value printed is the fraction of flux remaining in the residual and can be considered a good indicator of a successful model and subtraction. Next are three columns displaying the original, the model, and the residual, respectively, for each Level2 image. Finally, there are corner plots suggesting the success of the fits (more documentation and explanation of these plots is coming).\n",
+ "\n",
+ "In this case, you will notice a systematic trend in the residuals. The PSF is oversubtracted in the centermost pixel and undersubtracted in the wings. The cause is unknown. It may be due to a poor PSF model. The user should consider generating more complex PSF models, but that is beyond the scope of this notebook. Nonetheless, the residual value is pretty good so the overall statistical error is relatively small."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c9a1b447",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10000, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50],\n",
+ " },\n",
+ " fit_width=7,\n",
+ " fit_centroid='pixel',\n",
+ " fit_bkg=True,\n",
+ " fit_flux='single'\n",
+ ")\n",
+ "\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()\n",
+ "\n",
+ "print(jwst_obs.psf_result.phot_cal_table)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "46b39817-d45e-4504-9e0a-281ef405f695",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "jwst_obs.psf_result.phot_cal_table"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31bd70f0",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# As noted above, As a result, the flux and corresponding error take into account a single overall fit. \n",
+ "# Therefore, there is no need to average the resulting magnitudes or errors. They should all be the same to within their individual zero-point differences (typically <1%).\n",
+ "mag_lvl2_arr = jwst_obs.psf_result.phot_cal_table['mag']\n",
+ "magerr_lvl2_arr = jwst_obs.psf_result.phot_cal_table['magerr']\n",
+ "\n",
+ "# Print Magnitude from Table\n",
+ "print(mag_lvl2_arr, '\\n\\n', magerr_lvl2_arr)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "af070d39-4c07-4801-b98f-dd9d3be0d7fe",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "jwst_obs_fast = space_phot.observation2(lvl2[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ed225d52-9d2c-4641-a37f-922e8fa696fc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "centers = [ref_x, ref_y]\n",
+ "jwst_obs_fast.fast_psf(psfs[0], centers)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a57f9275",
+ "metadata": {},
+ "source": [
+ "### 3.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "62a15eb8-2a9d-4f5a-895a-b18dc33b24e4",
+ "metadata": {},
+ "source": [
+ "Despite the above discussion on performing PSF photometry on the pre-mosaiced data products, space_phot has the functionality to create a mosaiced Level3 PSF at a given single position on the detector based on the Level2 images. The advantage to this is the ability to perform PSF photometry on the deep, stacked data in cases where faint sources are expected to have prohibitively low signal-to-noise in Level2 data. The disadvantage is the amount of time required to make mosaiced Level3 PSF, so that this method is most useful when dealing with a small number of low signal-to-noise sources.
\n",
+ "\n",
+ "Useful references:
\n",
+ "Space-Phot Documentation on Level3 Fitting: https://space-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#level-3-psf
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7bd91d15",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level3 data file the same as above\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31eb83ad-b752-4a51-ac9a-6e44f59495f9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0cd06c0a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "source_location = SkyCoord('17:43:04.4879', '+66:55:01.837', unit=(u.hourangle, u.deg))\n",
+ "\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.5, max_cut=30)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c1537e88-f7db-4dd4-846e-a8f66c33bb83",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7bea076d",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# The function get_jwst_psf is a space_phot wrapper for the WebbPSF calc_psf function and uses a lot of the same keywords.\n",
+ "# There are more advanced methods for generating your WebbPSF, but those are beyond the scope of this notebook.\n",
+ "# The defaults used by get_jwst_psf in this notebook are:\n",
+ "# oversample=4\n",
+ "# normalize='last'\n",
+ "# Non-distorted PSF\n",
+ "# Useful reference: https://webbpsf.readthedocs.io/en/latest/api/webbpsf.JWInstrument.html#webbpsf.JWInstrument.calc_psf\n",
+ "\n",
+ "# Get PSF from WebbPSF\n",
+ "jwst3_obs = space_phot.observation3(lvl3[0])\n",
+ "psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d126d410-7a21-45e9-b0ad-8b9a8308a174",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psf3.data, (161, 161), (200, 200))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.01)\n",
+ "\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model (Mosaiced)')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "990505bf-184e-456f-9a7e-14df8316414c",
+ "metadata": {},
+ "source": [
+ "#### Notes on the PSF Fitting in Space_Phot:
\n",
+ "\n",
+ "https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "As noted above, improved documentation will be coming. For now, here are some important points to consider.\n",
+ "See detailed notes in Section 3.1 above about the fitting process and diagnostics\n",
+ "\n",
+ "In addition, consider here that jwst3_obs is generating a Level3 PSF by using the JWST pipeline to resample and combine multiple Level2 PSFs. The Level2 PSFs are generated at the precise location of the source in each Level2 file to account for detector level effects. The resampling uses default resampling paramters. However, users should be aware that if they performed customized resampling for their Level2 data products, they should use similar resampling steps for their PSF below."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27525a0a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "# See detailed notes in Section 3.1 above about the fitting process and diagnostics\n",
+ "jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10000, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50],\n",
+ " },\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_flux=True\n",
+ ")\n",
+ "\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "dc1f930a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "mag_lvl3psf = jwst3_obs.psf_result.phot_cal_table['mag'][0]\n",
+ "magerr_lvl3psf = jwst3_obs.psf_result.phot_cal_table['magerr'][0]\n",
+ "\n",
+ "print(round(mag_lvl2_arr[0], 4), round(magerr_lvl2_arr[0], 4))\n",
+ "print(round(mag_lvl3psf, 5), round(magerr_lvl3psf, 5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4920c8b1-da54-434c-a8b9-4427c3157a62",
+ "metadata": {},
+ "source": [
+ "## Good agreement between Level2 and level3 results!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b5f0ad5-b59e-4eff-8687-f6a2199d8bd9",
+ "metadata": {},
+ "source": [
+ "4.-Faint/Upper Limit, Single Object\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1dc60da1-0f4d-4e5e-a109-f7352dfd0fdc",
+ "metadata": {},
+ "source": [
+ "The purpose of this section is to illustrate how to calculate an upper limit using PSF photometry a blank part of the sky. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8af6f83a-5925-44d5-be0f-facd2316d1ca",
+ "metadata": {},
+ "source": [
+ "### 4.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29ac6f64",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files\n",
+ "lvl3 = ['mast/01028/jw01028-o006_t001_miri_f770w_i2d.fits']\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3ebedb35",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create Level 2 Data List from ASN files\n",
+ "prefix = \"./mast/01028/\"\n",
+ "asn = glob.glob(prefix+'jw01028-o006_*_image3_00004_asn.json')\n",
+ "\n",
+ "with open(asn[0], \"r\") as fi:\n",
+ " lvl2 = []\n",
+ " for ln in fi:\n",
+ " if ln.startswith(' \"expname\":'):\n",
+ " x = ln[2:].split(':')\n",
+ " y = x[1].split('\"')\n",
+ " lvl2.append(prefix+y[1])\n",
+ "\n",
+ "print(lvl2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "322b5a27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "\n",
+ "# Reference for JWST DQ Flag Definitions: https://jwst-pipeline.readthedocs.io/en/latest/jwst/references_general/references_general.html\n",
+ "# In this case, we choose all DQ > 10, but users are encouraged to choose their own values accordingly.\n",
+ "for file in lvl2:\n",
+ " ref_fits = ImageModel(file)\n",
+ " data = ref_fits.data\n",
+ " dq = ref_fits.dq\n",
+ " data[dq >= 10] = np.nan\n",
+ " ref_fits.data = data\n",
+ " ref_fits.save(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2b5c7fea-3df3-4434-92cc-c7d8afa531dc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Examine the First Image (After DQ Flags Set)\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6f4ed7dc-bb03-4dca-9908-2130d96f7c63",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "be4db7ec-4328-4f34-ab37-75d0c537f3f0",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Examine the First Image\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a7f4f725-9314-41e4-8359-c746fd789799",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "72b8c907",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# The function get_jwst_psf is a space_phot wrapper for the WebbPSF calc_psf function and uses a lot of the same keywords.\n",
+ "# There are more advanced methods for generating your WebbPSF, but those are beyond the scope of this notebook.\n",
+ "# The defaults used by get_jwst_psf in this notebook are:\n",
+ "# oversample=4\n",
+ "# normalize='last'\n",
+ "# Non-distorted PSF\n",
+ "# Useful reference: https://webbpsf.readthedocs.io/en/latest/api/webbpsf.JWInstrument.html#webbpsf.JWInstrument.calc_psf\n",
+ "\n",
+ "# Get PSF from WebbPSF\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "psfs = space_phot.get_jwst_psf(jwst_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "04793a1d-5406-4516-9db8-5845a9f97194",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psf3.data, (161, 161), (200, 200))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.01)\n",
+ "\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a2615569",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "# https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10, 1000],\n",
+ " 'bkg': [0, 50],\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=True,\n",
+ " fit_centroid='fixed',\n",
+ " fit_flux='single'\n",
+ ")\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()\n",
+ "\n",
+ "print(jwst_obs.psf_result.phot_cal_table)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "234c8a45",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# As noted above, As a result, the flux and corresponding error take into account a single overall fit. \n",
+ "# Therefore, there is no need to average the resulting magnitudes or errors. They should all be the same to within their individual zero-point differences (typically <1%).\n",
+ "\n",
+ "# Print Upper Limits\n",
+ "magupper_lvl2psf = jwst_obs.upper_limit(nsigma=5)\n",
+ "magupper_lvl2psf"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e325db03-6e9b-4f04-be7b-5e80063dd9b8",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "### 4.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2e7ce46d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level3 data file the same as above.\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ffe34f40-6e67-4357-af85-908f92deb889",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_data, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "26c66637-1ee7-44b2-92a0-c042ac91e10d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "ref_wcs = ref_fits.get_fits_wcs()\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, ref_wcs)\n",
+ "ref_cutout = extract_array(ref_data, (21, 21), (ref_x, ref_y))\n",
+ "\n",
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=4.5, max_cut=5)\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d6ccfece-2bc7-443d-8755-92270331d1c3",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c70e1903-ef78-4efe-9f62-a46967b44b35",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "source_location = SkyCoord('17:43:00.0332', '+66:54:42.677', unit=(u.hourangle, u.deg))\n",
+ "\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006 (level 3)')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "50fbb856",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# The function get_jwst_psf is a space_phot wrapper for the WebbPSF calc_psf function and uses a lot of the same keywords.\n",
+ "# There are more advanced methods for generating your WebbPSF, but those are beyond the scope of this notebook.\n",
+ "# The defaults used by get_jwst_psf in this notebook are:\n",
+ "# oversample=4\n",
+ "# normalize='last'\n",
+ "# Non-distorted PSF\n",
+ "# Useful reference: https://webbpsf.readthedocs.io/en/latest/api/webbpsf.JWInstrument.html#webbpsf.JWInstrument.calc_psf\n",
+ "\n",
+ "# Get PSF from WebbPSF\n",
+ "jwst3_obs = space_phot.observation3(lvl3[0])\n",
+ "psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "37eaaa85-da28-4dd6-a1c5-727b718f1831",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# The scale should highlight the background noise so it is possible to see all faint sources.\n",
+ "ref_cutout = extract_array(psf3.data, (161, 161), (200, 200))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='log', min_cut=0.0, max_cut=0.01)\n",
+ "\n",
+ "plt.imshow(ref_cutout, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.title('WebbPSF Model')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fe699262",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-1000, 1000],\n",
+ " 'bkg': [0, 50],\n",
+ " },\n",
+ " fit_width=9,\n",
+ " fit_bkg=True,\n",
+ " fit_centroid=False,\n",
+ " fit_flux=True\n",
+ ")\n",
+ "\n",
+ "jwst3_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst3_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ecaec8db",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "magupper_lvl3psf = jwst3_obs.upper_limit(nsigma=5)\n",
+ "print(round(magupper_lvl2psf[0], 4))\n",
+ "print(round(magupper_lvl3psf[0], 5))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e4996d2-6274-473d-b13c-f3848c27ad78",
+ "metadata": {},
+ "source": [
+ "## Note you can go significantly deeper with the Level3 combined data product"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a969717-bbef-40b9-ac9b-f83dec99dc09",
+ "metadata": {},
+ "source": [
+ "5.-Stellar Field (LMC)\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "da877310-fd47-41d6-afea-7fa725a546af",
+ "metadata": {},
+ "source": [
+ "#### In this case, we are going to do the same steps as in Section 3, but for multiple stars. The purpose is to illustrate the workflow and runtime for using space_phot on a large number of stars. We suggest that space_phot may be less optimal for large numbers of bright stars. Other programs, such as DOLPHOT or Photutils, may be better suited for this use case. The primary advantage to space_phot is on faint, single sources. But it can be extended to a larger number if desired."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32bdafe6-db19-4080-9587-b9785c2f7fa7",
+ "metadata": {},
+ "source": [
+ "### 5.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b618756f",
+ "metadata": {},
+ "source": [
+ "##### Now do the same thing for a larger group of stars and test for speed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "838bd76d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files\n",
+ "lvl3 = [\"./mast/01171/jw01171-o004_t001_miri_f560w_i2d.fits\"]\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "73aba802",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 2 Files\n",
+ "lvl2 = glob.glob('./mast/01171/jw01171004*cal.fits')\n",
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "57f9d790",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Find Stars in Level 3 File\n",
+ "\n",
+ "# Get rough estimate of background (There are Better Ways to Do Background Subtraction)\n",
+ "bkgrms = MADStdBackgroundRMS()\n",
+ "mmm_bkg = MMMBackground()\n",
+ "\n",
+ "ref_fits = ImageModel(lvl3[0])\n",
+ "w = ref_fits.get_fits_wcs()\n",
+ "\n",
+ "std = bkgrms(ref_fits.data)\n",
+ "bkg = mmm_bkg(ref_fits.data)\n",
+ "data_bkgsub = ref_fits.data.copy()\n",
+ "data_bkgsub -= bkg \n",
+ "fwhm_psf = 1.882 # pixels for F560W\n",
+ "threshold = 5.\n",
+ "\n",
+ "daofind = DAOStarFinder(threshold=threshold * std, fwhm=fwhm_psf, exclude_border=True, min_separation=10)\n",
+ "\n",
+ "found_stars = daofind(data_bkgsub)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e4cee97c",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "found_stars.pprint_all(max_lines=10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cb42faab-f55f-41fb-9145-65adc8cb28b5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot the found stars\n",
+ "norm = simple_norm(data_bkgsub, 'sqrt', percent=99)\n",
+ "fig, ax = plt.subplots(figsize=(10, 10))\n",
+ "ax.imshow(data_bkgsub, origin='lower', norm=norm)\n",
+ "\n",
+ "xypos = zip(found_stars['xcentroid'], found_stars['ycentroid'])\n",
+ "aper = CircularAperture(xypos, r=10)\n",
+ "aper.plot(ax, color='red')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7c7d793b",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Filter out only stars you want\n",
+ "plt.figure(figsize=(12, 8))\n",
+ "plt.clf()\n",
+ "\n",
+ "ax1 = plt.subplot(2, 1, 1)\n",
+ "\n",
+ "ax1.set_xlabel('mag')\n",
+ "ax1.set_ylabel('sharpness')\n",
+ "\n",
+ "xlim0 = np.min(found_stars['mag']) - 0.25\n",
+ "xlim1 = np.max(found_stars['mag']) + 0.25\n",
+ "ylim0 = np.min(found_stars['sharpness']) - 0.15\n",
+ "ylim1 = np.max(found_stars['sharpness']) + 0.15\n",
+ "\n",
+ "ax1.set_xlim(xlim0, xlim1)\n",
+ "ax1.set_ylim(ylim0, ylim1)\n",
+ "\n",
+ "ax1.scatter(found_stars['mag'], found_stars['sharpness'], s=10, color='k')\n",
+ "\n",
+ "sh_inf = 0.40\n",
+ "sh_sup = 0.82\n",
+ "#mag_lim = -5.0\n",
+ "lmag_lim = -3.0\n",
+ "umag_lim = -5.0\n",
+ "\n",
+ "ax1.plot([xlim0, xlim1], [sh_sup, sh_sup], color='r', lw=3, ls='--')\n",
+ "ax1.plot([xlim0, xlim1], [sh_inf, sh_inf], color='r', lw=3, ls='--')\n",
+ "ax1.plot([lmag_lim, lmag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "ax1.plot([umag_lim, umag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "\n",
+ "ax2 = plt.subplot(2, 1, 2)\n",
+ "\n",
+ "ax2.set_xlabel('mag')\n",
+ "ax2.set_ylabel('roundness')\n",
+ "\n",
+ "ylim0 = np.min(found_stars['roundness2']) - 0.25\n",
+ "ylim1 = np.max(found_stars['roundness2']) - 0.25\n",
+ "\n",
+ "ax2.set_xlim(xlim0, xlim1)\n",
+ "ax2.set_ylim(ylim0, ylim1)\n",
+ "\n",
+ "round_inf = -0.40\n",
+ "round_sup = 0.40\n",
+ "\n",
+ "ax2.scatter(found_stars['mag'], found_stars['roundness2'], s=10, color='k')\n",
+ "\n",
+ "ax2.plot([xlim0, xlim1], [round_sup, round_sup], color='r', lw=3, ls='--')\n",
+ "ax2.plot([xlim0, xlim1], [round_inf, round_inf], color='r', lw=3, ls='--')\n",
+ "ax2.plot([lmag_lim, lmag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "ax2.plot([umag_lim, umag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "\n",
+ "plt.tight_layout()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6ac852af",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "mask = ((found_stars['mag'] < lmag_lim) & (found_stars['mag'] > umag_lim) & (found_stars['roundness2'] > round_inf)\n",
+ " & (found_stars['roundness2'] < round_sup) & (found_stars['sharpness'] > sh_inf) \n",
+ " & (found_stars['sharpness'] < sh_sup) & (found_stars['xcentroid'] > 100) & (found_stars['xcentroid'] < 700)\n",
+ " & (found_stars['ycentroid'] > 100) & (found_stars['ycentroid'] < 700))\n",
+ "\n",
+ "found_stars_sel = found_stars[mask]\n",
+ "\n",
+ "print('Number of stars found originally:', len(found_stars))\n",
+ "print('Number of stars in final selection:', len(found_stars_sel))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "567f81f5",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "found_stars_sel"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7fd293e-a8f4-4c34-ab99-23d71adee080",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Convert pixel to wcs coords\n",
+ "skycoords = w.pixel_to_world(found_stars_sel['xcentroid'], found_stars_sel['ycentroid'])\n",
+ "len(skycoords)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6c46e19",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "for file in lvl2:\n",
+ " ref_fits = ImageModel(file)\n",
+ " data = ref_fits.data\n",
+ " dq = ref_fits.dq\n",
+ " data[dq >= 10] = np.nan\n",
+ " ref_fits.data = data\n",
+ " ref_fits.save(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5516a64f",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create a grid for fast lookup using WebbPSF. The larger the number of grid points, the better the photometric precision.\n",
+ "# Developer note. Would be great to have a fast/approximate look up table. \n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "grid = space_phot.util.get_jwst_psf_grid(jwst_obs, num_psfs=16)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "38760103-e702-4b6b-bd5e-7ed12c67a6d8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = QTable([skycoords], names=[\"skycoord\"])\n",
+ "t.write('skycoord.ecsv', overwrite=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b85e222f",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now Loop Through All Stars and Build Photometry Table\n",
+ "# Readers should refer to all diagnostics discussed above. \n",
+ "# It should be noted that empty plots correspond to LVL2 files with dither positions that do not cover that particular coordinate.\n",
+ "warnings.simplefilter('ignore')\n",
+ "counter = 0.\n",
+ "badindex = []\n",
+ "\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "localbkg_estimator = LocalBackground(5, 10, bkg_estimator=MMMBackground())\n",
+ "\n",
+ "for source_location in skycoords:\n",
+ " tic = time.perf_counter()\n",
+ " print('Starting', counter+1., ' of', len(skycoords), ':', source_location)\n",
+ " psfs = space_phot.util.get_jwst_psf_from_grid(jwst_obs, source_location, grid)\n",
+ " xys = [jwst_obs.wcs_list[i].world_to_pixel(source_location) for i in range(jwst_obs.n_exposures)]\n",
+ " bkg = [localbkg_estimator(jwst_obs.data_arr_pam[i], xys[i][0], xys[i][0]) for i in range(jwst_obs.n_exposures)]\n",
+ " print(bkg)\n",
+ " jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-100000, 100000],\n",
+ " 'centroid': [-2.0, 2.0],\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=False,\n",
+ " fit_centroid='wcs',\n",
+ " background=bkg,\n",
+ " fit_flux='single',\n",
+ " maxiter=None\n",
+ " )\n",
+ " \n",
+ " jwst_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ " ra = jwst_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst_obs.psf_result.phot_cal_table['dec'][0]\n",
+ "\n",
+ " fit_location = SkyCoord(ra, dec, unit=u.deg)\n",
+ " \n",
+ " jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " fit_location,\n",
+ " bounds={\n",
+ " 'flux': [-100000, 100000],\n",
+ " 'centroid': [-2.0, 2.0],\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=False,\n",
+ " background=bkg,\n",
+ " fit_centroid='fixed',\n",
+ " fit_flux='single',\n",
+ " maxiter=None\n",
+ " )\n",
+ "\n",
+ " jwst_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ " ra = jwst_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " mag_arr = jwst_obs.psf_result.phot_cal_table['mag']\n",
+ " magerr_arr = jwst_obs.psf_result.phot_cal_table['magerr']\n",
+ " mag_lvl2psf = np.mean(mag_arr)\n",
+ " magerr_lvl2psf = math.sqrt(sum(p**2 for p in magerr_arr))\n",
+ "\n",
+ " if counter == 0:\n",
+ " df = pd.DataFrame(np.array([[ra, dec, mag_lvl2psf, magerr_lvl2psf]]), columns=['ra', 'dec', 'mag', 'magerr'])\n",
+ " else:\n",
+ " df = pd.concat([df, pd.DataFrame(np.array([[ra, dec, mag_lvl2psf, magerr_lvl2psf]]), columns=['ra', 'dec', 'mag', 'magerr'])], ignore_index=True)\n",
+ " counter = counter + 1.\n",
+ " \n",
+ " toc = time.perf_counter()\n",
+ " print(\"Elapsed Time for Photometry:\", toc - tic)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "13518464-e91b-4fa0-9e64-9cc84b0a56ef",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xys"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d4fbc1e5-96c5-48bb-b463-5af8970f1a2f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "dcd3bc1b-0263-4fb6-8217-108f4a88be4e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Write to File\n",
+ "df.to_csv('miri_photometry_space_phot_lvl2.txt', index=False) "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3604e260-2da4-4f43-b306-fb7cd65e738b",
+ "metadata": {},
+ "source": [
+ "### 5.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24dbbba6-6d1a-40b2-9028-de916cdc76e4",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3[0]\n",
+ "\n",
+ "ref_fits = ImageModel(ref_image)\n",
+ "ref_data = ref_fits.data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=0.5, max_cut=5)\n",
+ "\n",
+ "plt.figure(figsize=(20, 12))\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "clb = plt.colorbar()\n",
+ "clb.set_label('MJy/Str', labelpad=-40, y=1.05, rotation=0)\n",
+ "plt.gca().tick_params(axis='both', color='none')\n",
+ "plt.xlabel('Pixels')\n",
+ "plt.ylabel('Pixels')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5be212d8-c43a-478e-98ed-b1877e44a347",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF and drizzle it to the source location\n",
+ "jwst3_obs = space_phot.observation3(lvl3[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e4c85327-4b59-4228-8610-4a85909fb397",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "lvl3[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "717c2fc5-1e72-48c7-98aa-4b8216aac567",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "skycoords"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1881aeea-d5c7-4e1c-9669-19351f9c1157",
+ "metadata": {},
+ "source": [
+ "#### Readers should refer to all diagnostics discussed above. In general, this loop shows the difficulty in doing PSF photometry on a wide variety of stars (brightness, distribution on the detector, etc) without visual inspection. Especially when dealing with low SNR sources.This is true for all photometry packages. Users should inspect the associated metrics and consider optimizing the parameters for specific stars of interest. Nonetheless, the success of the fits can always be quantified in with the associated error bars.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "922accc4-2179-4e03-ad60-2beeb594faea",
+ "metadata": {
+ "scrolled": true,
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now Loop Through All Stars and Build Photometry Table\n",
+ "counter = 0.\n",
+ "badindex = []\n",
+ "\n",
+ "for source_location in skycoords[1:3]:\n",
+ " tic = time.perf_counter()\n",
+ " print('Starting', counter+1., ' of', len(skycoords), ':', source_location)\n",
+ " psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location, num_psfs=4)\n",
+ " xys = [jwst3_obs.wcs.world_to_pixel(source_location)]\n",
+ " bkg = [localbkg_estimator(jwst3_obs.data, xys[0][0], xys[0][1])]\n",
+ " print(bkg)\n",
+ " jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-100000, 100000],\n",
+ " 'centroid': [-2.0, 2.0],\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=False,\n",
+ " background=bkg,\n",
+ " fit_flux=True\n",
+ " )\n",
+ "\n",
+ " ra = jwst3_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst3_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " fit_location = SkyCoord(ra, dec, unit=u.deg)\n",
+ " jwst3_obs.aperture_photometry(fit_location, encircled_energy=70)\n",
+ " print(jwst3_obs.aperture_result.phot_cal_table['mag'])\n",
+ " \n",
+ " jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " fit_location,\n",
+ " bounds={\n",
+ " 'flux': [-100000, 100000],\n",
+ " 'centroid': [-2.0, 2.0],\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=False,\n",
+ " fit_centroid=False,\n",
+ " background=bkg,\n",
+ " fit_flux=True\n",
+ " )\n",
+ "\n",
+ " jwst3_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ "\n",
+ " ra = jwst3_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst3_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " mag_lvl3psf = jwst3_obs.psf_result.phot_cal_table['mag'][0]\n",
+ " magerr_lvl3psf = jwst3_obs.psf_result.phot_cal_table['magerr'][0]\n",
+ "\n",
+ " if counter == 0:\n",
+ " df = pd.DataFrame(np.array([[ra, dec, mag_lvl3psf, magerr_lvl3psf]]), columns=['ra', 'dec', 'mag', 'magerr'])\n",
+ " else:\n",
+ " df = pd.concat([df, pd.DataFrame(np.array([[ra, dec, mag_lvl3psf, magerr_lvl3psf]]), columns=['ra', 'dec', 'mag', 'magerr'])], ignore_index=True)\n",
+ " counter = counter + 1.\n",
+ " toc = time.perf_counter()\n",
+ " print(\"Elapsed Time for Photometry:\", toc - tic)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "db250a73-d9a6-4f38-a42b-335f66d73bde",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4cb0557a-dee2-4be3-9513-83bb9671d71e",
+ "metadata": {},
+ "source": [
+ "**Important Note**: When not to use. Due to the sensitivity of the space_phot parameters, this tool is not meant to be used for a large sample of stars (i.e., Section 5 below). If a user would like to use space_phot on more than one source, they should carefully construct a table of parameters that are carefully refined for each source."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5af6df8c-85ea-4aac-b6b7-53ac630fa3e0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5630029f-31d1-42cd-8454-225e86cabc48",
+ "metadata": {},
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "843b5201-6f57-46f0-9da0-b738714178d3",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ },
+ "toc-showcode": false
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/MIRI/psf_photometry/requirements.txt b/notebooks/MIRI/psf_photometry/requirements.txt
new file mode 100644
index 000000000..b18db10ec
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/requirements.txt
@@ -0,0 +1,9 @@
+astropy >= 5.3.1
+crds>=12.0.4
+jwst>=1.11.4
+numpy>=1.25.2
+pandas>=2.1.0
+photutils>=1.11.0
+matplotlib>=3.7.2
+space-phot>=0.2.5
+webbpsf>=1.2.1
\ No newline at end of file
diff --git a/notebooks/MIRI/psf_photometry/skycoord.ecsv b/notebooks/MIRI/psf_photometry/skycoord.ecsv
new file mode 100644
index 000000000..669c6336b
--- /dev/null
+++ b/notebooks/MIRI/psf_photometry/skycoord.ecsv
@@ -0,0 +1,37 @@
+# %ECSV 1.0
+# ---
+# datatype:
+# - {name: skycoord.ra, unit: deg, datatype: float64}
+# - {name: skycoord.dec, unit: deg, datatype: float64}
+# meta: !!omap
+# - __serialized_columns__:
+# skycoord:
+# __class__: astropy.coordinates.sky_coordinate.SkyCoord
+# dec: !astropy.table.SerializedColumn
+# __class__: astropy.coordinates.angles.core.Latitude
+# unit: &id001 !astropy.units.Unit {unit: deg}
+# value: !astropy.table.SerializedColumn {name: skycoord.dec}
+# frame: icrs
+# ra: !astropy.table.SerializedColumn
+# __class__: astropy.coordinates.angles.core.Longitude
+# unit: *id001
+# value: !astropy.table.SerializedColumn {name: skycoord.ra}
+# wrap_angle: !astropy.coordinates.Angle
+# unit: *id001
+# value: 360.0
+# representation_type: spherical
+# schema: astropy-2.0
+skycoord.ra skycoord.dec
+80.68751894520042 -69.44522201115608
+80.70519786193715 -69.43753355085107
+80.69947280902996 -69.44357618680897
+80.70076349113887 -69.44322911964474
+80.70480666335257 -69.44154063210087
+80.69801843690163 -69.4475452383858
+80.70685608541822 -69.44434739569299
+80.7213461835945 -69.44277393265011
+80.72825455016927 -69.44591832679221
+80.7313996806274 -69.44867713838086
+80.72803699363548 -69.45133844442186
+80.73746269558515 -69.44890937080869
+80.74189341069834 -69.44834497289607
diff --git a/notebooks/NIRCam/NIRCam_WFSS_Box_extraction/BoxExtraction_using_Grismconf_CRDS.ipynb b/notebooks/NIRCam/NIRCam_WFSS_Box_extraction/BoxExtraction_using_Grismconf_CRDS.ipynb
new file mode 100755
index 000000000..31d8fcd5e
--- /dev/null
+++ b/notebooks/NIRCam/NIRCam_WFSS_Box_extraction/BoxExtraction_using_Grismconf_CRDS.ipynb
@@ -0,0 +1,1166 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "4d6f827d-b694-4875-b3a0-e6ef001c602e",
+ "metadata": {},
+ "source": [
+ "# WFSS Box Extraction Example"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9e5bd923-8f7f-40b8-a86f-2b7e3f5d5351",
+ "metadata": {},
+ "source": [
+ "This notebook demonstrates how to use the Generalized World Coordinate System (gWCS) in a Wide Field Slitless Spectrscopy (WFSS) observation to determine source locations and wavelengths. It shows how to calculate the location of a source in a WFSS observation given its location in a corresponding imaging observation, and how to calculate the wavelength at a particular pixel along an object's trace.\n",
+ "\n",
+ "It then shows how to use the gWCS to perform a box extraction of a spectrum and translate the 1D spectrum into physical units.\n",
+ "\n",
+ "In this example, we use exposures from JWST program 01076. We want to work on files that have full gWCS information in their headers, and that have had the flat field applied. We also need to run the flux calibration step of the pipeline in order to populate the name of the photom reference file in the header of the WFSS file (in the S_PHOTOM header keyword). This reference file will be used as part of the extraction process below. The photom step will not change the values of the science data in the WFSS exposure, because the observing mode (OBS_MODE header keyword) in the file is set to NRC_WFSS.\n",
+ "\n",
+ "In order to accomplish this, the assign_wcs, flat field, and photom steps of the pipeline must be run on the data. Ordinarily this means we could simply download *_cal.fits files from MAST, and that is true for the imaging mode data used in this notebook. However as we show below, we want to apply the imaging mode flat field to the WFSS data. This means that we must download the *_rate.fits file, and manually run these pipeline steps on the data. For consistency, we do the same with the imaging mode data.\n",
+ "\n",
+ "JWST detectors show little to no wavelength dependence in their flat-field, and just as is regularly done with HST WFSS data, in this example we have the pipeline apply the flat field for the direct cross filter to all the imaging as well as WFSS observations. We do not use a WFSS-specific flat field.\n",
+ "\n",
+ "Once the data have been properly calibrated, the notebook uses the grismconf package to translate between source locations in the imaging and WFSS data, and calculate wavelengths associated with a given location in the WFSS data. grismconf also uses the flux calibration curve in the photom reference file for the grisms to translate the data from units of $DN/sec$ to $F_{lambda}$ units ($erg / sec / cm^2 / \\overset{\\circ}{A}$). grismconf will obtain the needed NIRCam WFSS configuration files from the Calibration Reference Data System (CRDS). Note that the photom step must be run on the data in order to obain the name of the approproate CRDS sesitivity file.\n",
+ "\n",
+ "Note: At this stage, the important part of this is not the absolute accuracy of the WCS. Instead, we rely on accurate self-consistency between the imaging and the WFSS observations. \n",
+ "\n",
+ "Author: N. Pirzkal
\n",
+ "Date created: 24 Sept 2024"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "89e0c28a-df34-4bfb-af97-9ddf9d5768b4",
+ "metadata": {},
+ "source": [
+ "## Table of Contents\n",
+ "1. [Package Imports](#Package-Imports)\n",
+ "2. [Define Functions and Parameters](#Define-Functions-and-Parameters)\n",
+ "3. [Download Data](#Download-Data)\n",
+ "4. [Run Pipeline Steps](#Run-Pipeline-Steps)\n",
+ "5. [Basic Computation of WFSS Information](#Basic-Computation-of-WFSS-Information)\n",
+ " * [Compute where light gets dispersed to](#Compute-where-light-gets-dispersed-to)\n",
+ " * [Compute the spectral trace for a given object](#Compute-the-spectral-trace-for-a-given-object)\n",
+ " * [Basic Box Extraction](#Basic-Box-Extraction)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7ffc9e2c-5958-4e61-8073-d584f8331c30",
+ "metadata": {},
+ "source": [
+ "## Package Imports"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3d34005b-94f5-42c0-b13b-cc10aea8a7f9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from copy import deepcopy\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "import os\n",
+ "import requests\n",
+ "from scipy.stats import sigmaclip\n",
+ "\n",
+ "import grismconf\n",
+ "from jwst.assign_wcs import AssignWcsStep\n",
+ "from jwst.flatfield import FlatFieldStep\n",
+ "from jwst.photom import PhotomStep"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d6b0fc4b",
+ "metadata": {},
+ "source": [
+ "## Set CRDS Path and Server"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bb09075d-b0ba-4bee-b6c5-a2f27b767b6a",
+ "metadata": {},
+ "source": [
+ "Before running the pipeline steps, we need to ensure our our CRDS environment is configured. This includes defining a CRDS cache directory in which to keep the reference files that will be used by the calibration pipeline.\n",
+ "\n",
+ "If the root directory for the local CRDS cache has not already been set, it will be created in the home directory."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ab935ab-a215-4fe3-8aad-b3dae5a017bb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Check whether the local CRDS cache directory has been set.\n",
+ "# If not, set it to the user home directory\n",
+ "if (os.getenv('CRDS_PATH') is None):\n",
+ " os.environ['CRDS_PATH'] = os.path.join(os.path.expanduser('~'), 'crds')\n",
+ "# Check whether the CRDS server URL has been set. If not, set it.\n",
+ "if (os.getenv('CRDS_SERVER_URL') is None):\n",
+ " os.environ['CRDS_SERVER_URL'] = 'https://jwst-crds.stsci.edu'\n",
+ "\n",
+ "# Echo CRDS path and context in use\n",
+ "print('CRDS local filepath:', os.environ['CRDS_PATH'])\n",
+ "print('CRDS file server:', os.environ['CRDS_SERVER_URL'])\n",
+ "\n",
+ "# import crds after setting up the required environment variables\n",
+ "from crds import client\n",
+ "if client.get_crds_server() != os.environ['CRDS_SERVER_URL']:\n",
+ " client.set_crds_server('https://jwst-crds.stsci.edu')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "117e4ec7-610c-4d11-82f4-756971dc23e0",
+ "metadata": {},
+ "source": [
+ "## Define Functions and Parameters"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "46833187-8b09-4abb-8317-572154b56700",
+ "metadata": {},
+ "source": [
+ "Define a function to download a named file via the MAST API to the current directory. The function includes authentication logic, but the example in this notebook uses public data, so no MAST API token is required."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a2b09d49-838f-4523-a6b2-9e678d10666a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_jwst_file(name, mast_api_token=None, overwrite=False):\n",
+ " \"\"\"Retrieve a JWST data file from MAST archive.\n",
+ " \n",
+ " Parameters\n",
+ " ----------\n",
+ " name : str\n",
+ " Name of the file to download from MAST\n",
+ " \n",
+ " mast_api_token : str\n",
+ " MAST API token. Required only for proprietary data\n",
+ " \n",
+ " overwrite : bool\n",
+ " If True and the requested file already exists locally, the file will not be downloaded. IF False,\n",
+ " the file will be downloaded\n",
+ " \"\"\"\n",
+ " # If the file already exists locally, don't redownload it, unless the\n",
+ " # user has set the overwrite keyword\n",
+ " if os.path.isfile(name):\n",
+ " if not overwrite:\n",
+ " print(f'{name} already exists locally. Skipping download.')\n",
+ " return\n",
+ " else:\n",
+ " print(f'{name} exists locally. Re-downloading.')\n",
+ "\n",
+ " mast_url = \"https://mast.stsci.edu/api/v0.1/Download/file\"\n",
+ " params = dict(uri=f\"mast:JWST/product/{name}\")\n",
+ " if mast_api_token:\n",
+ " headers = dict(Authorization=f\"token {mast_api_token}\")\n",
+ " else:\n",
+ " headers = {}\n",
+ " r = requests.get(mast_url, params=params, headers=headers, stream=True)\n",
+ " r.raise_for_status()\n",
+ " with open(name, \"wb\") as fobj:\n",
+ " for chunk in r.iter_content(chunk_size=1024000):\n",
+ " fobj.write(chunk)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c3f0f371-8912-425a-8911-7eec9b46f841",
+ "metadata": {},
+ "source": [
+ "Define a function that will run assign_wcs and flat fielding on an input rate file"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0de8bf4a-3f12-466c-8887-f7d1bed0f543",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def run_pipeline_steps(filename):\n",
+ " \"\"\"Run the assign_wcs, flat field, and photom calibration steps on the given file.\n",
+ " If the file contains WFSS data, trick the pipeline to use the imaging mode flat\n",
+ " field reference file.\n",
+ " \n",
+ " Parameters\n",
+ " ----------\n",
+ " filename : str\n",
+ " Name of the input file upon which the steps will be run\n",
+ " \n",
+ " Returns\n",
+ " -------\n",
+ " filename : str\n",
+ " Name of the output file saved by the pipeline steps\n",
+ " \n",
+ " photom : jwst.datamodels.ImageModel\n",
+ " Datamodel instance containing the calibrated data\n",
+ " \"\"\"\n",
+ " assign_wcs = AssignWcsStep.call(filename)\n",
+ "\n",
+ " # In order to apply the imaging mode flat field reference file to the data,\n",
+ " # we need to trick CRDS by temporarily changing the pupil value to be CLEAR\n",
+ " reset_pupil = False\n",
+ " if 'GRISM' in assign_wcs.meta.instrument.pupil:\n",
+ " true_pupil = deepcopy(assign_wcs.meta.instrument.pupil)\n",
+ " assign_wcs.meta.instrument.pupil = 'CLEAR'\n",
+ " reset_pupil = True\n",
+ "\n",
+ " # Run the flat field step\n",
+ " flat = FlatFieldStep.call(assign_wcs, save_results=True)\n",
+ " \n",
+ " # Run the photom step to populate the name of the WFSS sensitivity \n",
+ " photom = PhotomStep.call(flat, save_results=True)\n",
+ " \n",
+ " # Set the pupil back to the original value now that flat fielding is complete\n",
+ " if reset_pupil:\n",
+ " photom.meta.instrument.pupil = true_pupil\n",
+ " photom.save(photom.meta.filename)\n",
+ " \n",
+ " # Return the name of the output file, as well as the datamodel\n",
+ " return photom.meta.filename, photom"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "41068262-9a54-46c5-a9d2-ca2217ad1fee",
+ "metadata": {},
+ "source": [
+ "## Download Data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3f7b6965-2313-45c3-9399-c8601be0fdbd",
+ "metadata": {},
+ "source": [
+ "Download an example imaging mode rate file and corresponding WFSS mode rate file from MAST."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "253df6eb-f6ab-4c53-8c0e-ad58f64cda9b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# First, download the imaging and WFSS files from MAST\n",
+ "imaging_file = \"jw01076103001_02102_00001_nrcalong_rate.fits\"\n",
+ "wfss_file = \"jw01076103001_02101_00001_nrcalong_rate.fits\"\n",
+ "get_jwst_file(imaging_file)\n",
+ "get_jwst_file(wfss_file)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ab5f3c78-258f-49c4-99c4-87e26f0972cf",
+ "metadata": {},
+ "source": [
+ "## Run Pipeline Steps"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d5c025ca-ebba-49c2-ab24-a6318a29b65c",
+ "metadata": {},
+ "source": [
+ "Run the assign_wcs, flat field, and photom calibration steps on both the imaging and WFSS files."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27a15b96-cfd7-4670-9142-a5378e053dd1",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "# Run AssignWcsStep, FlatFieldStep, and PhotomStep on the imaging rate file\n",
+ "imaging_flat_file, imaging_data = run_pipeline_steps(imaging_file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "67401d19-bbcf-45fc-b2a0-06c133f998e0",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "# Run AssignWcsStep, FlatFieldStep, and PhotomStep on the WFSS rate file\n",
+ "wfss_flat_file, wfss_data = run_pipeline_steps(wfss_file)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5da7a2f7-7546-4791-ae9f-7ff0953af46a",
+ "metadata": {},
+ "source": [
+ "## Basic Computation of WFSS Information"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "455e24ae-791b-4331-85e8-b28d62a287fe",
+ "metadata": {},
+ "source": [
+ "All computations for WFSS are performed in detector coordinate space. All of the characteristics of the dispersed traces, including any change in the relative positions and the global shape (e.g. curvature, offsets...) of the traces is handled using a series of straight forward equations. This is described in ISR WFC3 2017-01: \"A more generalized coordinate transformation approach for grisms\".\n",
+ "Here we assume that a source would be at the pixel coordinates of ($x$, $y$). The coordinate of a single pixel on on the dispersed trace for the same source is denoted as ($x_g$, $y_g$) and the relative position of this dispersed trace element is therefore offset (x$_g$-x, \n",
+ " y$_g$-y) pixels with respect to the position of the source. The functional relation between ($x$, $y$), ($x_g$, $y_g$) and the wavelength of the light $\\lambda$, as well as their inverses are:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "\\delta x = x_g - x = f_x(x,y;t)\\\\\n",
+ "\\delta y = y_g - y = f_y(x,y;t)\\\\\n",
+ "\\lambda = f_\\lambda(x,y;t)\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "and \n",
+ "$$\n",
+ "\\begin{align}\n",
+ "t = f^{-1}_x(x,y;\\delta x)\\\\\n",
+ "t = f^{-1}_y(x,y;\\delta y)\\\\\n",
+ "t = f^{-1}_\\lambda(x,y;\\lambda)\n",
+ "\\end{align}\n",
+ "$$"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "83ea3d86-19fc-4529-b551-22a2ec98818f",
+ "metadata": {},
+ "source": [
+ "Note that these functions are parametrized with respect to the parameter $t$. This allows for some flexibility on the part of the calibration effort as $t$ can be defined somewhat arbitrarilly. In the case of the NIRCam grisms however, $t$ was chosen to be the $\\delta x$ or $\\delta y$, for the GRISMR and GRISMC, respectively since these grisms disperse light along the x-direction and y-direction, respectively. However, for additional convenience, the $t$ parameter is normalized to unity so that values of $t = 0$ and $t = 1$ correspond to the blue and red light edges of a dispersed spectrum.\n",
+ "Using the 6 equations above, one can relate any combination of ($x$,$y$), ($x'$,$y'$), $t$, and $\\lambda$ values. The equations listed above are implemented as DISPX(), DISPY(), DISPL(), INVDISPX(), INVDISPY(), and INVDISPL() in the GRISMCONF package."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d179fcbb-9d08-4eed-a3d4-29d716823a8f",
+ "metadata": {},
+ "source": [
+ "Now we will use the Grismconf package to retrieve information about the WFSS file. Note that we are using the output file from the calibration steps above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fe3fd942-7839-44c7-9ad9-a188e2a1d942",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# This is the final output file from the pipeline call on the WFSS file above\n",
+ "wfss_file = \"jw01076103001_02101_00001_nrcalong_photomstep.fits\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "513059b9-0ebd-4431-a942-b6182fcc64c7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Load a WFSS configuration file to use in the example below.\n",
+ "C = grismconf.Config(wfss_file)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "edca7185-8e19-445b-b45d-1617e52cd75f",
+ "metadata": {},
+ "source": [
+ "### Compute where light gets dispersed to"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "077da68e-0367-4e80-9adb-97794a5b951f",
+ "metadata": {},
+ "source": [
+ "Here we show how to calculate the location of the point on the trace corresponding to a given wavelength for a source at a given detector location ($x$, $y$). For these calculations, we need only the WFSS file. The corresponding imaging mode file is not necessary."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7ef508ca-a7c6-4b8b-a089-110b652984e4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x = 1000 # Pixel x coordinate\n",
+ "y = 1000 # Pixel y coordinate\n",
+ "\n",
+ "wavelength = 3.5 # wavelength, in microns"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "31248889-2e95-469f-8526-f2b89c83e4fa",
+ "metadata": {},
+ "source": [
+ "We want to compute $\\hat x$, the amount of dispersion in a pixel for photons with a wavelength of $\\lambda$. We first use the relation between $t$ and $\\lambda$ and then the relation between $\\hat x$ and $t$. This is done using INVDISPL() for order \"+1\" for an object at location ($x$, $y$):"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ea7ebf64-6469-43bc-9176-b50b957223b0",
+ "metadata": {},
+ "source": [
+ "Check which orders are available"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e50b4915-34a3-4161-b7e4-f15c514f8619",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "C.orders"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c5a89da8-1694-40a6-af02-b166ea20810d",
+ "metadata": {},
+ "source": [
+ "Calculate $t$ for the given position and wavelength."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "251e5c79-22a9-4f09-9f31-a3410b025fb2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = C.INVDISPL(\"+1\", x, y, wavelength)\n",
+ "print(\"t =\", t)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a1f1617a-7bc1-42cf-bb35-76dfeb8c78a3",
+ "metadata": {},
+ "source": [
+ "We now can compute $\\delta x$ and $\\delta y$ using DISPX():"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c2b817a4-0da0-4bf7-84d9-683a4e72979a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "𝛿x = C.DISPX(\"+1\", x, y, t)\n",
+ "𝛿y = C.DISPY(\"+1\", x, y, t)\n",
+ "print(\"𝛿x =\", 𝛿x)\n",
+ "print(\"𝛿y =\", 𝛿y)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "de14b4c7-a8c3-486f-9147-b4cfe7a1908f",
+ "metadata": {},
+ "source": [
+ "The final pixel coordinates are therefore:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e5283aa4-fd40-45d9-a865-fe05f17005e8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "xg = x + 𝛿x\n",
+ "yg = y + 𝛿y\n",
+ "print(\"Trace coordinates:\", xg, yg)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0482f604-97b7-4c90-952e-09ffd8ba78de",
+ "metadata": {},
+ "source": [
+ "Alternatively, we could compute the approximate wavelength of the light at a given position on the spectral trace. For example, we would like to compute the wavelength of a pixel that is at coordinates ($x_g$, $y_g$) for a 1st order spectrum of a source that is known to be at the coordinates ($x$, $y$). As this is a Grism R spectrum, we can use the relation between $\\delta x$ and t and $\\lambda$. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c77920de-3dc0-429a-a266-de8a95b20ae6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Source is at the coordinates (1000, 1000) and we are looking at a pixel\n",
+ "# along the trace at pixel coordinate 1558\n",
+ "x = 1000\n",
+ "y = 1000"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f7b33090-f6ca-4fe9-bae1-055e708f8f23",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t = C.INVDISPX(\"+1\", x, y, xg-x)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1fe1df61-db4a-4594-8722-1364e2a9e2ae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "wavelength = C.DISPL(\"+1\", x, y, t)\n",
+ "print(f\"Wavelength = {wavelength} microns\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7a1550bd-572f-4662-bfef-5f3b1fac82d1",
+ "metadata": {},
+ "source": [
+ "Here we see that we get back the 3.5 micron wavelength that we used as input when calculating $x_g$ and $y_g$ above."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2abb689b-06b8-461d-bd9a-d79ff590b568",
+ "metadata": {},
+ "source": [
+ "### Compute the spectral trace for a given object"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6ac1665e-3fd0-4c5f-9251-0a6474be9afc",
+ "metadata": {},
+ "source": [
+ "We can compute where we would expect the dispersed 1st order trace for a given object in a similar manner. We can use a series of $t$ values to cover the whole spectra trace (in this case the NIRCam calibration assumes $0= 6.1.3
+grismconf >= 1.51
+jupyter >= 1.1.1
+jwst >= 1.16.0
+matplotlib >= 3.9.2
+numpy == 1.26.4
+requests >= 2.32.3
+scipy >= 1.14.1
+crds >= 12.0.4
\ No newline at end of file
diff --git a/notebooks/NIRCam/psf_photometry/requirements.txt b/notebooks/NIRCam/psf_photometry/requirements.txt
index e28207cdb..c8469e320 100644
--- a/notebooks/NIRCam/psf_photometry/requirements.txt
+++ b/notebooks/NIRCam/psf_photometry/requirements.txt
@@ -6,4 +6,4 @@ photutils>=2.0.2
ipywidgets>=8.1.1
matplotlib>=3.7.2
webbpsf>=1.2.1
-stsynphot>=1.2.0
+stsynphot>=1.2.0
\ No newline at end of file
diff --git a/notebooks/NIRCam/psf_photometry_with_space_phot/nircam_spacephot.ipynb b/notebooks/NIRCam/psf_photometry_with_space_phot/nircam_spacephot.ipynb
new file mode 100644
index 000000000..a24a396b5
--- /dev/null
+++ b/notebooks/NIRCam/psf_photometry_with_space_phot/nircam_spacephot.ipynb
@@ -0,0 +1,1081 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "b58d6954",
+ "metadata": {},
+ "source": [
+ "# NIRCam PSF Photometry With Space_Phot\n",
+ "\n",
+ "**Author**: Ori Fox\n",
+ "
\n",
+ "**Last Updated**: August, 2023"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "88c61bcf-1c4d-407a-b80c-aa13a01fd746",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Table of contents\n",
+ "1. [Introduction](#intro)
\n",
+ "2. [Setup](#setup)
\n",
+ " 2.1 [Python imports](#py_imports)
\n",
+ " 2.2 [Download data](#bso4)
\n",
+ "3. [Bright, Single Object](#bso)
\n",
+ " 3.1 [Multiple, Level2 Files](#bso2)
\n",
+ "4. [Faint/Upper Limit, Single Object](#fso)
\n",
+ " 4.1 [Multiple, Level2 Files](#fso2)
\n",
+ "5. [Stellar Field (LMC)](#lmv)
\n",
+ " 5.1 [Multiple, Level2 Files](#lmc2)
\n",
+ " 5.2 [Single, Level3 Mosaicked File](#lmc3)
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f572688",
+ "metadata": {},
+ "source": [
+ "1.-Introduction \n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "95891849",
+ "metadata": {},
+ "source": [
+ "**Packages to Install**:\n",
+ "drizzlepac\\\\\n",
+ "space_phot https://github.com/jpierel14/space_phot\\\\\n",
+ "photutils (on main git+https://github.com/astropy/photutils)\\\\\n",
+ "jupyter\\\\\n",
+ "\n",
+ "**Goals**: \n",
+ "\n",
+ "PSF Photometry can be obtained using:\n",
+ "\n",
+ "* grid of PSF models from WebbPSF\n",
+ "* single effective PSF (ePSF) NOT YET AVAILABLE\n",
+ "* grid of effective PSF NOT YET AVAILABLE\n",
+ "\n",
+ "The notebook shows:\n",
+ "\n",
+ "* how to obtain the PSF model from WebbPSF (or build an ePSF)\n",
+ "* how to perform PS\n",
+ "* photometry on the image"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1cf3d18f",
+ "metadata": {},
+ "source": [
+ "2.-Setup \n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b762602",
+ "metadata": {},
+ "source": [
+ "### 2.1-Python imports ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c50eace",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from astropy.io import fits\n",
+ "from astropy.nddata import extract_array\n",
+ "from astropy.coordinates import SkyCoord\n",
+ "from astropy import wcs\n",
+ "from astropy.wcs.utils import skycoord_to_pixel\n",
+ "from astropy import units as u\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "from astropy.visualization import simple_norm\n",
+ "from urllib.parse import urlparse\n",
+ "import requests\n",
+ "import time\n",
+ "import math\n",
+ "import logging\n",
+ "from jwst.associations import load_asn\n",
+ "import matplotlib.pyplot as plt\n",
+ "%matplotlib inline\n",
+ "\n",
+ "from astroquery.mast import Observations\n",
+ "import os\n",
+ "import tarfile\n",
+ "\n",
+ "# Background and PSF Functions\n",
+ "from photutils.background import MMMBackground, MADStdBackgroundRMS\n",
+ "from photutils.detection import DAOStarFinder\n",
+ "\n",
+ "import space_phot\n",
+ "from importlib.metadata import version\n",
+ "version('space_phot')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5a9b5cbd-5999-43be-9e48-e0abcf726a01",
+ "metadata": {},
+ "source": [
+ "### 2.2-Download data ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "545860a5-cca4-4185-82fb-e4d962690be8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Download NIRCam Data PID 1537 (Calibration Program) and NIRCam Data PID 1476 (LMC)\n",
+ "files_to_download = [\n",
+ " 'jw01537-o024_t001_nircam_clear-f444w-sub160_i2d.fits',\n",
+ " 'jw01537-o024_20241003t130922_image3_00001_asn.json',\n",
+ " 'jw01537024001_0310a_00001_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310a_00002_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310a_00003_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310a_00004_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310k_00001_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310k_00002_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310k_00003_nrcblong_cal.fits',\n",
+ " 'jw01537024001_0310k_00004_nrcblong_cal.fits',\n",
+ " 'jw01476-o001_t001_nircam_clear-f150w_i2d.fits',\n",
+ " 'jw01476-o001_20240910t004333_image3_00023_asn.json',\n",
+ " 'jw01476001007_02101_00001_nrca1_cal.fits',\n",
+ " 'jw01476001007_02101_00002_nrca1_cal.fits',\n",
+ " 'jw01476001007_02101_00003_nrca1_cal.fits',\n",
+ " 'jw01476001008_02101_00001_nrca1_cal.fits',\n",
+ " 'jw01476001008_02101_00002_nrca1_cal.fits',\n",
+ " 'jw01476001008_02101_00003_nrca1_cal.fits',\n",
+ " 'jw01476001008_02101_00004_nrca1_cal.fits',\n",
+ " 'jw01476001008_02101_00005_nrca1_cal.fits',\n",
+ " 'jw01476001008_02101_00006_nrca1_cal.fits'\n",
+ "]\n",
+ "\n",
+ "\n",
+ "def download_files(files_to_download):\n",
+ " for file in files_to_download:\n",
+ " # Check if the file already exists in the current working directory\n",
+ " if os.path.exists(file):\n",
+ " print(f\"File {file} already exists. Skipping download.\")\n",
+ " continue\n",
+ " cal_uri = f'mast:JWST/product/{file}'\n",
+ " Observations.download_file(cal_uri)\n",
+ "\n",
+ "\n",
+ "# Call the function to download files\n",
+ "download_files(files_to_download)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5611799d",
+ "metadata": {},
+ "source": [
+ "3.-Bright, Single Object\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "55c52f95",
+ "metadata": {},
+ "source": [
+ "### 3.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8f44a6cf",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files: NIRCam Data PID 1537 (Calibration Program):\n",
+ "lvl3 = 'jw01537-o024_t001_nircam_clear-f444w-sub160_i2d.fits'\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e7a50b5b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hdl = fits.open(lvl3)\n",
+ "hdr = hdl[0].header\n",
+ "asnfile = hdr['ASNTABLE']\n",
+ "lvl2_prelim = []\n",
+ "asn_data = load_asn(open(asnfile))\n",
+ "for member in asn_data['products'][0]['members']:\n",
+ " #print(member['expname'])\n",
+ " lvl2_prelim.append(member['expname'])\n",
+ " \n",
+ "lvl2_prelim"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "68a7181e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Sort out LVL2 Data That Includes The Actual Source (there are 4 detectors)\n",
+ "source_location = SkyCoord('5:05:30.6593', '+52:49:49.862', unit=(u.hourangle, u.deg))\n",
+ "lvl2 = []\n",
+ "for ref_image in lvl2_prelim:\n",
+ " print(ref_image)\n",
+ " ref_fits = fits.open(ref_image)\n",
+ " ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ " ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ " print(ref_y, ref_x)\n",
+ " try:\n",
+ " extract_array(ref_data, (11, 11), (ref_x, ref_y)) # block raising an exception\n",
+ " except Exception as e:\n",
+ " logging.error(f\"An error occurred: {e}\")\n",
+ " pass # Doing nothing on exception, but logging it\n",
+ " else:\n",
+ " lvl2.append(ref_image)\n",
+ " print(ref_image + ' added to final list')\n",
+ " \n",
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cc2ae63a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "for file in lvl2:\n",
+ " hdul = fits.open(file, mode='update')\n",
+ " data = fits.open(file)['SCI', 1].data\n",
+ " dq = fits.open(file)['DQ', 1].data\n",
+ " data[dq == 1] = np.nan\n",
+ " hdul['SCI', 1].data = data\n",
+ " hdul.flush()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "37bb2026",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Examine the First Image\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()\n",
+ "lvl2[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f647dfde",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Zoom in to see the source\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-10, max_cut=1000)\n",
+ "plt.imshow(ref_cutout, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.title('PID1537,Obs024')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()\n",
+ "\n",
+ "ref_cutout"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d67d57b9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Set environmental variables\n",
+ "os.environ[\"WEBBPSF_PATH\"] = \"./webbpsf-data/webbpsf-data\"\n",
+ "os.environ[\"PYSYN_CDBS\"] = \"./grp/redcat/trds/\"\n",
+ "\n",
+ "# required webbpsf data\n",
+ "boxlink = 'https://stsci.box.com/shared/static/qxpiaxsjwo15ml6m4pkhtk36c9jgj70k.gz' \n",
+ "boxfile = './webbpsf-data/webbpsf-data-LATEST.tar.gz'\n",
+ "synphot_url = 'http://ssb.stsci.edu/trds/tarfiles/synphot5.tar.gz'\n",
+ "synphot_file = './synphot5.tar.gz'\n",
+ "\n",
+ "webbpsf_folder = './webbpsf-data'\n",
+ "synphot_folder = './grp'\n",
+ "\n",
+ "\n",
+ "def download_file(url, dest_path, timeout=60):\n",
+ " parsed_url = urlparse(url)\n",
+ " if parsed_url.scheme not in [\"http\", \"https\"]:\n",
+ " raise ValueError(f\"Unsupported URL scheme: {parsed_url.scheme}\")\n",
+ "\n",
+ " response = requests.get(url, stream=True, timeout=timeout)\n",
+ " response.raise_for_status()\n",
+ " with open(dest_path, \"wb\") as f:\n",
+ " for chunk in response.iter_content(chunk_size=8192):\n",
+ " f.write(chunk)\n",
+ "\n",
+ "\n",
+ "# Gather webbpsf files\n",
+ "psfExist = os.path.exists(webbpsf_folder)\n",
+ "if not psfExist:\n",
+ " os.makedirs(webbpsf_folder)\n",
+ " download_file(boxlink, boxfile)\n",
+ " gzf = tarfile.open(boxfile)\n",
+ " gzf.extractall(webbpsf_folder, filter='data')\n",
+ "\n",
+ "# Gather synphot files\n",
+ "synExist = os.path.exists(synphot_folder)\n",
+ "if not synExist:\n",
+ " os.makedirs(synphot_folder)\n",
+ " download_file(synphot_url, synphot_file)\n",
+ " gzf = tarfile.open(synphot_file)\n",
+ " gzf.extractall('./', filter='data')\n",
+ "\n",
+ "# Get PSF from WebbPSF\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "psfs = space_phot.get_jwst_psf(jwst_obs, source_location)\n",
+ "plt.imshow(psfs[0].data)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c9a1b447",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "# https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "jwst_obs.psf_photometry(psfs, source_location, bounds={'flux': [-10, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50]},\n",
+ " fit_width=5,\n",
+ " fit_bkg=True,\n",
+ " fit_flux='single')\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()\n",
+ "\n",
+ "print(jwst_obs.psf_result.phot_cal_table)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "31bd70f0",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Calculate Average Magnitude from Table\n",
+ "mag_arr = jwst_obs.psf_result.phot_cal_table['mag']\n",
+ "magerr_arr = jwst_obs.psf_result.phot_cal_table['magerr']\n",
+ "\n",
+ "mag_lvl2psf = np.mean(mag_arr)\n",
+ "magerr_lvl2psf = math.sqrt(sum(p**2 for p in magerr_arr))\n",
+ "print(round(mag_lvl2psf, 4), round(magerr_lvl2psf, 4))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5b5f0ad5-b59e-4eff-8687-f6a2199d8bd9",
+ "metadata": {},
+ "source": [
+ "4.-Faint/Upper Limit, Single Object\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8af6f83a-5925-44d5-be0f-facd2316d1ca",
+ "metadata": {},
+ "source": [
+ "### 4.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ba101fd1",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files\n",
+ "lvl3 = 'jw01537-o024_t001_nircam_clear-f444w-sub160_i2d.fits'\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7725111b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from jwst.associations import load_asn\n",
+ "hdl = fits.open(lvl3)\n",
+ "hdr = hdl[0].header\n",
+ "asnfile = hdr['ASNTABLE']\n",
+ "lvl2_prelim = []\n",
+ "asn_data = load_asn(open(asnfile))\n",
+ "for member in asn_data['products'][0]['members']:\n",
+ " lvl2_prelim.append(member['expname'])\n",
+ " \n",
+ "lvl2_prelim"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a3536472",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Sort out LVL2 Data That Includes The Actual Source (there are 4 detectors)\n",
+ "source_location = SkyCoord('5:05:30.6186', '+52:49:49.130', unit=(u.hourangle, u.deg))\n",
+ "lvl2 = []\n",
+ "for ref_image in lvl2_prelim:\n",
+ " print(ref_image)\n",
+ " ref_fits = fits.open(ref_image)\n",
+ " ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ " ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ " print(ref_y, ref_x)\n",
+ " try:\n",
+ " extract_array(ref_data, (11, 11), (ref_x, ref_y)) # block raising an exception\n",
+ " except Exception as e:\n",
+ " logging.error(f\"An error occurred: {e}\")\n",
+ " pass # Doing nothing on exception, but logging it\n",
+ " else:\n",
+ " lvl2.append(ref_image)\n",
+ " print(ref_image + ' added to final list')\n",
+ " \n",
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "322b5a27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "for file in lvl2:\n",
+ " hdul = fits.open(file, mode='update')\n",
+ " data = fits.open(file)['SCI', 1].data\n",
+ " dq = fits.open(file)['DQ', 1].data\n",
+ " data[dq == 1] = np.nan\n",
+ " hdul['SCI', 1].data = data\n",
+ " hdul.flush()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "273fcf6c",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Examine the First Image\n",
+ "ref_image = lvl2[0]\n",
+ "print(ref_image)\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=25)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower', norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6ceb5541",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Pick a blank part of the sky to calculate the upper limit\n",
+ "ref_y, ref_x = skycoord_to_pixel(source_location, wcs.WCS(ref_fits['SCI', 1], ref_fits))\n",
+ "ref_cutout = extract_array(ref_data, (11, 11), (ref_x, ref_y))\n",
+ "norm1 = simple_norm(ref_cutout, stretch='linear', min_cut=-1, max_cut=25)\n",
+ "plt.imshow(ref_cutout, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.title('PID1028,Obs006')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "72b8c907",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "psfs = space_phot.get_jwst_psf(jwst_obs, source_location)\n",
+ "plt.imshow(psfs[0].data)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a2615569",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Do PSF Photometry using space_phot (details of fitting are in documentation)\n",
+ "# https://st-phot.readthedocs.io/en/latest/examples/plot_a_psf.html#jwst-images\n",
+ "jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-10, 1000],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=True,\n",
+ " fit_centroid='fixed',\n",
+ " fit_flux='single'\n",
+ ")\n",
+ "\n",
+ "jwst_obs.plot_psf_fit()\n",
+ "plt.show()\n",
+ "\n",
+ "jwst_obs.plot_psf_posterior(minweight=.0005)\n",
+ "plt.show()\n",
+ "\n",
+ "print(jwst_obs.psf_result.phot_cal_table)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "234c8a45",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Print Upper Limits\n",
+ "magupper_lvl2psf = jwst_obs.upper_limit(nsigma=5)\n",
+ "magupper_lvl2psf"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9a969717-bbef-40b9-ac9b-f83dec99dc09",
+ "metadata": {},
+ "source": [
+ "5.-Stellar Field (LMC)\n",
+ "------------------"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32bdafe6-db19-4080-9587-b9785c2f7fa7",
+ "metadata": {},
+ "source": [
+ "### 5.1-Multiple, Level2 Files ###"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b618756f",
+ "metadata": {},
+ "source": [
+ "##### Now do the same thing for a larger group of stars and test for speed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "838bd76d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Level 3 Files: NIRCam Data PID 1476 (LMC)\n",
+ "lvl3 = 'jw01476-o001_t001_nircam_clear-f150w_i2d.fits'\n",
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ee3c3389",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "hdl = fits.open(lvl3)\n",
+ "hdr = hdl[0].header\n",
+ "asnfile = hdr['ASNTABLE']\n",
+ "lvl2 = []\n",
+ "asn_data = load_asn(open(asnfile))\n",
+ "for member in asn_data['products'][0]['members']:\n",
+ " lvl2.append(member['expname'])\n",
+ " \n",
+ "lvl2 = [s for s in lvl2 if \"nrca1\" in s]\n",
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "57f9d790",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Find Stars in Level 3 File\n",
+ "# Get rough estimate of background (There are Better Ways to Do Background Subtraction)\n",
+ "bkgrms = MADStdBackgroundRMS()\n",
+ "mmm_bkg = MMMBackground()\n",
+ "\n",
+ "im = fits.open(lvl3) \n",
+ "w = wcs.WCS(im['SCI', 1])\n",
+ "\n",
+ "std = bkgrms(im[1].data)\n",
+ "bkg = mmm_bkg(im[1].data)\n",
+ "data_bkgsub = im[1].data.copy()\n",
+ "data_bkgsub -= bkg \n",
+ "sigma_psf = 1.636 # pixls for F770W\n",
+ "threshold = 5.\n",
+ "\n",
+ "daofind = DAOStarFinder(threshold=threshold * std, fwhm=sigma_psf, exclude_border=True)\n",
+ "found_stars = daofind(data_bkgsub)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e4cee97c",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "found_stars.pprint_all(max_lines=10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7c7d793b",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Filter out only stars you want\n",
+ "plt.figure(figsize=(12, 8))\n",
+ "plt.clf()\n",
+ "\n",
+ "ax1 = plt.subplot(2, 1, 1)\n",
+ "\n",
+ "ax1.set_xlabel('mag')\n",
+ "ax1.set_ylabel('sharpness')\n",
+ "\n",
+ "xlim0 = np.min(found_stars['mag']) - 0.25\n",
+ "xlim1 = np.max(found_stars['mag']) + 0.25\n",
+ "ylim0 = np.min(found_stars['sharpness']) - 0.15\n",
+ "ylim1 = np.max(found_stars['sharpness']) + 0.15\n",
+ "\n",
+ "ax1.set_xlim(xlim0, xlim1)\n",
+ "ax1.set_ylim(ylim0, ylim1)\n",
+ "\n",
+ "ax1.scatter(found_stars['mag'], found_stars['sharpness'], s=10, color='k')\n",
+ "\n",
+ "sh_inf = 0.40\n",
+ "sh_sup = 0.82\n",
+ "lmag_lim = -1.0\n",
+ "umag_lim = -6.0\n",
+ "\n",
+ "ax1.plot([xlim0, xlim1], [sh_sup, sh_sup], color='r', lw=3, ls='--')\n",
+ "ax1.plot([xlim0, xlim1], [sh_inf, sh_inf], color='r', lw=3, ls='--')\n",
+ "ax1.plot([lmag_lim, lmag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "ax1.plot([umag_lim, umag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "\n",
+ "ax2 = plt.subplot(2, 1, 2)\n",
+ "\n",
+ "ax2.set_xlabel('mag')\n",
+ "ax2.set_ylabel('roundness')\n",
+ "\n",
+ "ylim0 = np.min(found_stars['roundness2']) - 0.25\n",
+ "ylim1 = np.max(found_stars['roundness2']) - 0.25\n",
+ "\n",
+ "ax2.set_xlim(xlim0, xlim1)\n",
+ "ax2.set_ylim(ylim0, ylim1)\n",
+ "\n",
+ "round_inf = -0.40\n",
+ "round_sup = 0.40\n",
+ "\n",
+ "ax2.scatter(found_stars['mag'], found_stars['roundness2'], s=10, color='k')\n",
+ "\n",
+ "ax2.plot([xlim0, xlim1], [round_sup, round_sup], color='r', lw=3, ls='--')\n",
+ "ax2.plot([xlim0, xlim1], [round_inf, round_inf], color='r', lw=3, ls='--')\n",
+ "ax2.plot([lmag_lim, lmag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "ax2.plot([umag_lim, umag_lim], [ylim0, ylim1], color='r', lw=3, ls='--')\n",
+ "\n",
+ "plt.tight_layout()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6ac852af",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "mask = ((found_stars['mag'] < lmag_lim) & (found_stars['mag'] > umag_lim) & (found_stars['roundness2'] > round_inf)\n",
+ " & (found_stars['roundness2'] < round_sup) & (found_stars['sharpness'] > sh_inf) \n",
+ " & (found_stars['sharpness'] < sh_sup) & (found_stars['xcentroid'] > 1940) & (found_stars['xcentroid'] < 2000)\n",
+ " & (found_stars['ycentroid'] > 1890) & (found_stars['ycentroid'] < 1960))\n",
+ "\n",
+ "found_stars_sel = found_stars[mask]\n",
+ "\n",
+ "print('Number of stars found originally:', len(found_stars))\n",
+ "print('Number of stars in final selection:', len(found_stars_sel))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "567f81f5",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "found_stars_sel"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3a62c53a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Convert pixel to wcs coords\n",
+ "skycoords = w.pixel_to_world(found_stars_sel['xcentroid'], found_stars_sel['ycentroid'])\n",
+ "len(skycoords)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "03b8ff39",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "lvl2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0195f1ce",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "file = lvl2[0]\n",
+ "dq = fits.open(file)['DQ', 1].data\n",
+ "dq[233, 340]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e6c46e19",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Change all DQ flagged pixels to NANs\n",
+ "for file in lvl2:\n",
+ " hdul = fits.open(file, mode='update')\n",
+ " data = fits.open(file)['SCI', 1].data\n",
+ " dq = fits.open(file)['DQ', 1].data\n",
+ " data[dq == 262657] = np.nan\n",
+ " data[dq == 262661] = np.nan\n",
+ " hdul['SCI', 1].data = data\n",
+ " hdul.flush()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5516a64f",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create a grid for fast lookup using WebbPSF. The larger the grid, the better the photometric precision.\n",
+ "# Developer note. Would be great to have a fast/approximate look up table.\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "grid = space_phot.util.get_jwst_psf_grid(jwst_obs, num_psfs=4)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b85e222f",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now Loop Through All Stars and Build Photometry Table\n",
+ "counter = 0.\n",
+ "badindex = []\n",
+ "\n",
+ "jwst_obs = space_phot.observation2(lvl2)\n",
+ "for source_location in skycoords:\n",
+ " tic = time.perf_counter()\n",
+ " print('Starting', counter+1., ' of', len(skycoords), ':', source_location)\n",
+ " psfs = space_phot.util.get_jwst_psf_from_grid(jwst_obs, source_location, grid)\n",
+ " jwst_obs.psf_photometry(\n",
+ " psfs,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-100, 1000],\n",
+ " 'centroid': [-2., 2.],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=3,\n",
+ " fit_bkg=False,\n",
+ " fit_flux='single',\n",
+ " maxiter=5000\n",
+ " )\n",
+ " \n",
+ " jwst_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ " \n",
+ " ra = jwst_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " mag_arr = jwst_obs.psf_result.phot_cal_table['mag']\n",
+ " magerr_arr = jwst_obs.psf_result.phot_cal_table['magerr']\n",
+ " mag_lvl2psf = np.mean(mag_arr)\n",
+ " magerr_lvl2psf = math.sqrt(sum(p**2 for p in magerr_arr))\n",
+ "\n",
+ " if counter == 0:\n",
+ " df = pd.DataFrame(np.array([[ra, dec, mag_lvl2psf, magerr_lvl2psf]]), columns=['ra', 'dec', 'mag', 'magerr'])\n",
+ " else:\n",
+ " df = pd.concat([df, pd.DataFrame(np.array([[ra, dec, mag_lvl2psf, magerr_lvl2psf]]))], ignore_index=True)\n",
+ " counter = counter + 1.\n",
+ " \n",
+ " toc = time.perf_counter()\n",
+ " print(\"Elapsed Time for Photometry:\", toc - tic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3604e260-2da4-4f43-b306-fb7cd65e738b",
+ "metadata": {},
+ "source": [
+ "### 5.2-Single, Level3 Mosaicked File ###"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a57e893d-92cb-4de6-8c64-69911b691246",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "lvl3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24dbbba6-6d1a-40b2-9028-de916cdc76e4",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now do the same photometry on the Level 3 Data\n",
+ "ref_image = lvl3\n",
+ "ref_fits = fits.open(ref_image)\n",
+ "ref_data = fits.open(ref_image)['SCI', 1].data\n",
+ "norm1 = simple_norm(ref_data, stretch='linear', min_cut=-1, max_cut=10)\n",
+ "\n",
+ "plt.imshow(ref_data, origin='lower',\n",
+ " norm=norm1, cmap='gray')\n",
+ "plt.gca().tick_params(labelcolor='none', axis='both', color='none')\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5be212d8-c43a-478e-98ed-b1877e44a347",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Get PSF from WebbPSF and drizzle it to the source location\n",
+ "# Develop Note: Need Grid Capability for Level3 Data\n",
+ "jwst3_obs = space_phot.observation3(lvl3)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "922accc4-2179-4e03-ad60-2beeb594faea",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Now Loop Through All Stars and Build Photometry Table\n",
+ "counter = 0.\n",
+ "badindex = []\n",
+ "\n",
+ "for source_location in skycoords:\n",
+ " tic = time.perf_counter()\n",
+ " print('Starting', counter+1., ' of', len(skycoords), ':', source_location)\n",
+ " psf3 = space_phot.get_jwst3_psf(jwst_obs, jwst3_obs, source_location, num_psfs=4)\n",
+ " jwst3_obs.psf_photometry(\n",
+ " psf3,\n",
+ " source_location,\n",
+ " bounds={\n",
+ " 'flux': [-1000, 10000],\n",
+ " 'centroid': [-2, 2],\n",
+ " 'bkg': [0, 50]\n",
+ " },\n",
+ " fit_width=5,\n",
+ " fit_bkg=True,\n",
+ " fit_flux=True\n",
+ " )\n",
+ "\n",
+ " jwst3_obs.plot_psf_fit()\n",
+ " plt.show()\n",
+ "\n",
+ " ra = jwst3_obs.psf_result.phot_cal_table['ra'][0]\n",
+ " dec = jwst3_obs.psf_result.phot_cal_table['dec'][0]\n",
+ " mag_lvl3psf = jwst3_obs.psf_result.phot_cal_table['mag'][0]\n",
+ " magerr_lvl3psf = jwst3_obs.psf_result.phot_cal_table['magerr'][0]\n",
+ "\n",
+ " if counter == 0:\n",
+ " df = pd.DataFrame(np.array([[ra, dec, mag_lvl3psf, magerr_lvl3psf]]), columns=['ra', 'dec', 'mag', 'magerr'])\n",
+ " else:\n",
+ " df = pd.concat([df, pd.DataFrame(np.array([[ra, dec, mag_lvl3psf, magerr_lvl3psf]]))], ignore_index=True)\n",
+ " counter = counter + 1.\n",
+ " toc = time.perf_counter()\n",
+ " print(\"Elapsed Time for Photometry:\", toc - tic)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5630029f-31d1-42cd-8454-225e86cabc48",
+ "metadata": {},
+ "source": [
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "843b5201-6f57-46f0-9da0-b738714178d3",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.10"
+ },
+ "toc-showcode": false
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/notebooks/NIRCam/psf_photometry_with_space_phot/requirements.txt b/notebooks/NIRCam/psf_photometry_with_space_phot/requirements.txt
new file mode 100644
index 000000000..4d5253379
--- /dev/null
+++ b/notebooks/NIRCam/psf_photometry_with_space_phot/requirements.txt
@@ -0,0 +1,10 @@
+numpy>=1.25.2
+pandas>=2.1.0
+jwst>=1.11.4
+astropy>=5.3.3
+photutils>=1.11.0
+ipywidgets>=8.1.1
+matplotlib>=3.7.2
+webbpsf>=1.2.1
+stsynphot>=1.2.0
+space_phot
\ No newline at end of file
diff --git a/notebooks/NIRISS/NIRISS_WFSS_advanced/01_niriss_wfss_image2_image3.ipynb b/notebooks/NIRISS/NIRISS_WFSS_advanced/01_niriss_wfss_image2_image3.ipynb
index 8d081ae33..581581d7b 100644
--- a/notebooks/NIRISS/NIRISS_WFSS_advanced/01_niriss_wfss_image2_image3.ipynb
+++ b/notebooks/NIRISS/NIRISS_WFSS_advanced/01_niriss_wfss_image2_image3.ipynb
@@ -642,7 +642,8 @@
"\n",
"# this aligns the image to use the WCS coordinates; \n",
"# the images need to be loaded first, but before adding markers\n",
- "imviz.link_data(link_type='wcs')\n",
+ "linking = imviz.plugins['Orientation']\n",
+ "linking.link_type = 'WCS'\n",
"\n",
"# also plot the associated catalog\n",
"# this needs to be a separate loop due to linking in imviz when using sky coordinates\n",
@@ -793,7 +794,7 @@
" imviz.load_data(img, data_label=title)\n",
"\n",
" # this aligns the image to use the WCS coordinates\n",
- " linking = imviz.plugins['Links Control']\n",
+ " linking = imviz.plugins['Orientation']\n",
" linking.link_type = 'WCS'\n",
"\n",
" # also plot the associated catalog\n",
@@ -1197,7 +1198,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.7"
+ "version": "3.11.10"
}
},
"nbformat": 4,
diff --git a/notebooks/NIRISS/niriss_imaging/niriss-imaging-tutorial.ipynb b/notebooks/NIRISS/niriss_imaging/niriss-imaging-tutorial.ipynb
index 0c27bb638..fe65c20d3 100755
--- a/notebooks/NIRISS/niriss_imaging/niriss-imaging-tutorial.ipynb
+++ b/notebooks/NIRISS/niriss_imaging/niriss-imaging-tutorial.ipynb
@@ -1,5 +1,16 @@
{
"cells": [
+ {
+ "cell_type": "markdown",
+ "id": "5bb1229e-417e-4c96-93f2-004e6a615586",
+ "metadata": {},
+ "source": [
+ "\n",
+ "
Important Notice:\n",
+ "
This notebook is now part of the JWST-pipeline-notebooks repository and will be removed from this repository by November 30, 2024. Please access the notebook at its new location.
\n",
+ "
"
+ ]
+ },
{
"cell_type": "markdown",
"id": "0393e357-9d9d-4516-b28d-4d335fad33a0",
@@ -977,7 +988,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.9"
+ "version": "3.11.3"
}
},
"nbformat": 4,
diff --git a/notebooks/NIRSpec/galaxy_redshift/redshift_fitting.ipynb b/notebooks/NIRSpec/galaxy_redshift/redshift_fitting.ipynb
index a69ba9c85..56b601362 100644
--- a/notebooks/NIRSpec/galaxy_redshift/redshift_fitting.ipynb
+++ b/notebooks/NIRSpec/galaxy_redshift/redshift_fitting.ipynb
@@ -1,12 +1,18 @@
{
"cells": [
+ {
+ "cell_type": "markdown",
+ "id": "e5f5559e-f4b3-4118-b3ec-930b787faad9",
+ "metadata": {},
+ "source": [
+ "# Redshift and Template Fitting"
+ ]
+ },
{
"cell_type": "markdown",
"id": "e6c242e1",
"metadata": {},
"source": [
- "# Redshift and Template Fitting\n",
- "\n",
"This notebook covers basic examples on how a user can measure the redshift of a source using the visualization tool [Jdaviz](https://jdaviz.readthedocs.io/en/latest/) or programmatically with [Specutils](https://specutils.readthedocs.io/en/latest/).\n",
"\n",
"**Use case:** measure the redshift of a galaxy from its spectrum using 2 different methods. \n",
@@ -27,7 +33,7 @@
" - [Run the cross correlation function](#run_crosscorr)\n",
"\n",
"**Author**: Camilla Pacifici (cpacifici@stsci.edu)
\n",
- "**Updated**: September 14, 2023"
+ "**Updated**: November 18, 2024"
]
},
{
@@ -174,7 +180,7 @@
"# Select a specific directory on your machine or a temporary directory\n",
"data_dir = tempfile.gettempdir()\n",
"# Get the file from MAST\n",
- "fn = \"jw02736-o007_s09239_nirspec_f170lp-g235m_x1d.fits\"\n",
+ "fn = \"jw02736-o007_s000009239_nirspec_f170lp-g235m_x1d.fits\"\n",
"result = Observations.download_file(f\"mast:JWST/product/{fn}\", local_path=f'{data_dir}/{fn}')\n",
"\n",
"fn_template = download_file('https://stsci.box.com/shared/static/3rkurzwl0l79j70ddemxafhpln7ljle7.dat', cache=True)"
@@ -656,7 +662,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.13"
+ "version": "3.11.10"
}
},
"nbformat": 4,
diff --git a/notebooks/NIRSpec/galaxy_redshift/requirements.txt b/notebooks/NIRSpec/galaxy_redshift/requirements.txt
index 0f29b127b..5bf98c8dd 100644
--- a/notebooks/NIRSpec/galaxy_redshift/requirements.txt
+++ b/notebooks/NIRSpec/galaxy_redshift/requirements.txt
@@ -1 +1 @@
-jdaviz
+jdaviz>=4.0
diff --git a/notebooks/cross_instrument/asdf_example/requirements.txt b/notebooks/cross_instrument/asdf_example/requirements.txt
index 94d17eb40..d5fbc8273 100644
--- a/notebooks/cross_instrument/asdf_example/requirements.txt
+++ b/notebooks/cross_instrument/asdf_example/requirements.txt
@@ -1,6 +1,8 @@
-asdf >= 3.4.0
+asdf >= 3.5.0
astrocut >= 0.11.1
-astropy >= 6.1.3
+astropy >= 7.0.0
gwcs >= 0.21.0
-matplotlib >= 3.9.2
+matplotlib >= 3.9.3
requests >= 2.32.3
+ipydatagrid
+jupyter