Photometric Redshifts#
We will learn how we can estimate the redshift of a star-forming galaxy using photometric filters through an interactive demo. This will lay the foundation for the Merian final project.
Prerequisites
Concept of spectrum and redshift
Important
You don’t need to run this notebook on google colab! Just play with the plots below on the webpage.
Show code cell source
%load_ext autoreload
%autoreload 2
import os, sys
from IPython.display import clear_output
import numpy as np
import matplotlib.pyplot as plt
from astropy.table import Table
from astropy.coordinates import SkyCoord
# We can beautify our plots by changing the matplotlib setting a little
plt.rcParams['font.size'] = 18
plt.rcParams['image.origin'] = 'lower'
plt.rcParams['figure.figsize'] = (8, 6)
plt.rcParams['figure.dpi'] = 90
plt.rcParams['axes.linewidth'] = 2
Show code cell source
required_packages = ['sedpy', 'bokeh'] # Define the required packages for this notebook
import sys
import subprocess
try:
import google.colab
IN_COLAB = True
except ImportError:
IN_COLAB = False
if IN_COLAB:
# Download utils.py
!wget -q -O /content/utils.py https://raw.githubusercontent.com/AstroJacobLi/ObsAstGreene/refs/heads/main/book/docs/utils.py
# Function to check and install missing packages
def install_packages(packages):
for package in packages:
try:
__import__(package)
except ImportError:
print(f"Installing {package}...")
subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
# Install any missing packages
install_packages(required_packages)
else:
# If not in Colab, adjust the path for local development
sys.path.append('../../')
# Load bokeh functions for interactive functionalities
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, Slider, CustomJS, Label, LabelSet, Span, Range1d, LinearAxis
from bokeh.layouts import column
from bokeh.io import output_notebook
# Get the directory right
if IN_COLAB:
from google.colab import drive
drive.mount('/content/drive/')
os.chdir('/content/drive/Shareddrives/AST207/data')
else:
os.chdir('../../_static/ObsAstroData/')
1. Spectrum of a star-forming galaxy#
Let’s first load the spectrum of a star-forming galaxy, explore it interactively. We also show a list of emission lines which are quite common in a star-forming galaxy. (Understanding how to write this interative framework is not required!!)
Show code cell source
wave_obs, spec_obs = np.loadtxt('./merian/sf_gal_spec_obs.txt')
# emission_lines = [('Lyα', 1215.24), ('[OII]', 3727), ('Hγ', 4341.68), ('Hβ', 4862.68),
# ('[OIII]', 4960.295), ('[OIII]', 5008.240), ('He I', 5875.624), ('[NII]', 6549.86),
# ('Hα', 6564.61), ('[NII]', 6585.27), ('[SII]', 6718.29), ('[SII]', 6732.67)]
emission_lines = [('Lyα', 1215.24),('Hβ', 4862.68), ('[OIII]', 4960.295), ('[OIII]', 5008.240), ('Hα', 6564.61)]
Show code cell source
output_notebook()
line_names = [line[0] for line in emission_lines]
line_waves = [line[1] for line in emission_lines]
line_y = [max(spec_obs) * 0.8] * len(emission_lines) # Position labels at 80% of max flux
# Create data sources
source = ColumnDataSource(data={'wave': wave_obs, 'spec': spec_obs})
line_source = ColumnDataSource(data={
'name': line_names,
'wave': line_waves,
'y': line_y
})
# Create the plot with dual y-axes
plot = figure(width=1000, height=500,
title="Spectrum of a star-forming galaxy",
x_axis_label='Wavelength (Å)',
y_axis_label='Flux',
x_range=(3000, 12000),
y_axis_type="linear")
# Add the spectrum line
plot.line('wave', 'spec', source=source, line_width=3, legend_label="Spectrum")
# Add vertical lines for emission features
vlines = []
for wave in line_waves:
vline = Span(location=wave, dimension='height', line_color='red',
line_dash='dashed', line_width=1)
plot.add_layout(vline)
vlines.append(vline)
# Add labels for emission lines
labels = LabelSet(x='wave', y='y', text='name',
source=line_source,
text_font_size='8pt',
text_baseline='middle',
text_align='center',
angle=45)
plot.add_layout(labels)
# Create the layout and show the plot
layout = column(plot)
show(layout)
You can use the zoom functions to explore different emission lines, etc.
Exercise 1
List the most prominent emission lines in the above spectrum, and measure their restframe wavelengths. You can use the “box zoom” button to the right of the interactive figure to see the lines better. Compare your measured wavelengths with literature (e.g., you can find a list of spectral lines at https://classic.sdss.org/dr6/algorithms/linestable.php).
Zoom into a wavelength range with no strong emission lines. You will find that the flux is not zero. These “smooth” component in the spectrum is called “continuum”. In the above spectrum, the continuum is much weaker than the H\(\alpha\) line, for instance.
Is the continuum perfectly flat from 4000A to 8000A? Or does it have a slope?
2. “Red”-shifting the spectrum#
Now let’s think about the effect of redshift. In the interactive plot below, you can change the redshift of the galaxy by sliding the redshift slider bar, and see how the spectrum changes. We also overlay the photometric filters from the Hyper-Suprime Cam (HSC).
max_z = 2
delta_z = 0.005
Show code cell source
# Get filter curves using sedpy
from sedpy import observate
filternames = ['hsc_g', 'hsc_r', 'hsc_i', 'hsc_z', 'hsc_y']
filters = observate.load_filters(filternames)
filter_waves = {}
filter_trans = {}
for filt in filters:
filter_waves[filt.name] = filt.wavelength
filter_trans[filt.name] = filt.transmission
# Create sources for filter curves
filter_sources = {}
for name in filter_waves.keys():
filter_sources[name] = ColumnDataSource(data={
'wave': filter_waves[name],
'trans': filter_trans[name]
})
output_notebook()
# Prepare data for emission lines
line_names = [line[0] for line in emission_lines]
line_waves = [line[1] for line in emission_lines]
line_y = [max(spec_obs) * 0.8] * len(emission_lines) # Position labels at 80% of max flux
# Create data sources
source = ColumnDataSource(data={'wave': wave_obs, 'spec': spec_obs})
line_source = ColumnDataSource(data={
'name': line_names,
'wave': line_waves,
'y': line_y
})
# Create the plot with dual y-axes
plot = figure(width=1000, height=500,
title="Spectrum of a star-forming galaxy",
x_axis_label='Wavelength (Å)',
y_axis_label='Flux',
x_range=(3000, 12000),
y_axis_type="linear")
# Add secondary y-axis for filter transmission
plot.extra_y_ranges = {"transmission": Range1d(start=0, end=1.1)}
plot.add_layout(LinearAxis(y_range_name="transmission", axis_label="Filter Transmission"), 'right')
# Add the spectrum line
plot.line('wave', 'spec', source=source, line_width=3, legend_label="Spectrum")
# Add filter curves with fill
colors = ['royalblue', '#2ca02c', '#d62728', '#9467bd', '#8c564b', 'darkviolet', 'darkorange'] # Different color for each filter
for (name, src), color in zip(filter_sources.items(), colors):
plot.varea('wave', y1=0, y2='trans', source=src,
color=color, alpha=0.1, y_range_name="transmission")
plot.line('wave', 'trans', source=src, color=color,
line_width=1.5, alpha=0.4,
y_range_name="transmission")
# Add label at peak transmission for HSC filters
if 'hsc' in name:
wave_arr = src.data['wave']
trans_arr = src.data['trans']
peak_idx = np.argsort(wave_arr)[len(wave_arr)//2]
peak_wave = wave_arr[peak_idx]
peak_trans = trans_arr[peak_idx]
label_text = name.upper().replace('_', '-')
label = Label(x=peak_wave, y=peak_trans-0.15,
text=label_text, text_font_size='12pt',
text_color=color, text_align='center',
y_range_name="transmission")
plot.add_layout(label)
# Add vertical lines for emission features
vlines = []
for wave in line_waves:
vline = Span(location=wave, dimension='height', line_color='red',
line_dash='dashed', line_width=1)
plot.add_layout(vline)
vlines.append(vline)
# Add labels for emission lines
labels = LabelSet(x='wave', y='y', text='name',
source=line_source,
text_font_size='8pt',
text_baseline='middle',
text_align='center',
angle=45)
plot.add_layout(labels)
# Create the slider
redshift_slider = Slider(start=0, end=max_z, value=0, step=delta_z, title="Redshift (z)", min_width=700)
# Create the callback
callback = CustomJS(args=dict(source=source,
line_source=line_source,
vlines=vlines,
wave=wave_obs,
spec=spec_obs), code="""
const z = cb_obj.value; // Get the current redshift value from the slider
// Update spectrum wavelengths
const redshifted_wave = wave.map(w => w * (1 + z));
source.data = { wave: redshifted_wave, spec: spec };
// Store original wavelengths in a constant array to prevent accumulation
const ORIGINAL_WAVES = [1215.24, 4862.68, 4960.295, 5008.24, 6564.61];
// Update emission line positions and labels using original wavelengths
const redshifted_line_waves = ORIGINAL_WAVES.map(w => w * (1 + z));
line_source.data['wave'] = redshifted_line_waves;
// Update vertical line positions
for (let i = 0; i < vlines.length; i++) {
vlines[i].location = redshifted_line_waves[i];
}
// Trigger updates
source.change.emit();
line_source.change.emit();
""")
redshift_slider.js_on_change('value', callback)
# Create the layout and show the plot
layout = column(redshift_slider, plot)
show(layout)
Use the interactive plot above to explore how the spectrum (especially the emission lines) changes with redshift. And try to answer the following questions:
Exercise 2
Set the redshift to \(z=0.5\), and answer the following questions:
Measure the wavelength of H\(\alpha\) and H\(\beta\) lines from the above interactive plot. You can use the “box zoom” button (the second button to the right of the plot) to see the lines better.
Since you already measured the restframe wavelengths of these two lines in Exercise 1, calculate the redshift based on the observed wavelength and restframe wavelength. Is this redshift consistent with \(z=0.5\)?
Exercise 3
Let’s first define the broad-band color (e.g., \(r-i\)) as the difference between the \(r\) and \(i\) magnitudes (i.e., the log of the flux ratios). When a strong emission line falls in a filter, the magnitude of this galaxy in that filter will be quite bright compared to the other filters.
For a galaxy with the above spectrum at \(z=0\), will you see strong emission lines in the \(i\)-band? What component (line emission or continuum) dominates the light in the \(i\)-band?
Now move the galaxy to a redshift of \(z=0.2\). Does any strong emission line fall in the \(i\)-band? If we compare the \(r-i\) color at this redshift with the \(r-i\) color at \(z=0\), do you expect a significant change? Why?
Does \(r-i\) color change a lot when you increase the redshift from 0.2 to 0.3? How about changing the redshift from 0.2 to 0.22?
From the above exercises, you might have noticed that the color change as a function of redshift can be useful in determining the redshift. Is \(r-i\) color capable of precisely determining the redshift of a galaxy (to a precision of \(\delta z \approx 0.05\))?
In the real Universe, when increasing the distance to a galaxy (i.e., increasing redshift), should the flux also change? Why?
3. Median-band filter to better constrain redshift#
As we saw above, the broad-band color can be used to estimate the redshift of a galaxy. However, the broad-band color is not very sensitive to redshift. We can do better by introducing narrower filters.
The Merian survey designed two medium-band filters (named N540 and N708). These two filters are shown in the demo below. Let’s see how these filters can help us better constrain the redshift of a galaxy. N540 is shown in purple, and N708 is shown in orange.
max_z = 5
delta_z = 0.005
Show code cell source
# Get filter curves using sedpy
from sedpy import observate
filternames = ['hsc_g', 'hsc_r', 'hsc_i', 'hsc_z', 'hsc_y']
filters = observate.load_filters(filternames)
filters += observate.load_filters(["merian_n540", 'merian_n708'], directory='./merian/')
filter_waves = {}
filter_trans = {}
for filt in filters:
filter_waves[filt.name] = filt.wavelength
filter_trans[filt.name] = filt.transmission
# Create sources for filter curves
filter_sources = {}
for name in filter_waves.keys():
filter_sources[name] = ColumnDataSource(data={
'wave': filter_waves[name],
'trans': filter_trans[name]
})
output_notebook()
# Create the plot with dual y-axes
plot = figure(width=1000, height=500,
title="Spectrum of a star-forming galaxy",
x_axis_label='Wavelength (Å)',
y_axis_label='Flux',
x_range=(3000, 12000),
y_axis_type="linear")
# Add secondary y-axis for filter transmission
plot.extra_y_ranges = {"transmission": Range1d(start=0, end=1.1)}
plot.add_layout(LinearAxis(y_range_name="transmission", axis_label="Filter Transmission"), 'right')
# Add the spectrum line
plot.line('wave', 'spec', source=source, line_width=3, legend_label="Spectrum")
# Add filter curves with fill and labels
colors = ['royalblue', '#2ca02c', '#d62728', '#9467bd', '#8c564b', 'darkviolet', 'darkorange'] # Different color for each filter
for (name, src), color in zip(filter_sources.items(), colors):
alpha = 0.1
line_width = 1.5
line_dash = 'solid'
show_in_legend = True
if 'merian' in name:
alpha = 0.4
line = plot.line('wave', 'trans', source=src, color=color,
line_width=2, alpha=alpha+0.3, y_range_name="transmission",
legend_label=f"Filter {name}")
else:
line = plot.line('wave', 'trans', source=src, color=color,
line_width=1, alpha=alpha+0.3, y_range_name="transmission")
plot.varea('wave', y1=0, y2='trans', source=src,
color=color, alpha=alpha, y_range_name="transmission")
if 'hsc' in name:
wave_arr = src.data['wave']
trans_arr = src.data['trans']
peak_idx = np.argsort(wave_arr)[len(wave_arr)//2]
peak_wave = wave_arr[peak_idx]
peak_trans = trans_arr[peak_idx]
label_text = name.upper().replace('_', '-')
label = Label(x=peak_wave, y=peak_trans-0.15,
text=label_text, text_font_size='12pt',
text_color=color, text_align='center',
y_range_name="transmission")
plot.add_layout(label)
# Add vertical lines for emission features
vlines = []
for wave in line_waves:
vline = Span(location=wave, dimension='height', line_color='red',
line_dash='dashed', line_width=1)
plot.add_layout(vline)
vlines.append(vline)
# Add labels for emission lines
labels = LabelSet(x='wave', y='y', text='name',
source=line_source,
text_font_size='8pt',
text_baseline='middle',
text_align='center',
angle=45)
plot.add_layout(labels)
# Create the slider
redshift_slider = Slider(start=0, end=max_z, value=0, step=delta_z, title="Redshift (z)", min_width=900)
# Create the callback
callback = CustomJS(args=dict(source=source,
line_source=line_source,
vlines=vlines,
wave=wave_obs,
spec=spec_obs), code="""
const z = cb_obj.value; // Get the current redshift value from the slider
// Update spectrum wavelengths
const redshifted_wave = wave.map(w => w * (1 + z));
source.data = { wave: redshifted_wave, spec: spec };
// Store original wavelengths in a constant array to prevent accumulation
const ORIGINAL_WAVES = [1215.24, 4862.68, 4960.295, 5008.24, 6564.61];
// Update emission line positions and labels using original wavelengths
const redshifted_line_waves = ORIGINAL_WAVES.map(w => w * (1 + z));
line_source.data['wave'] = redshifted_line_waves;
// Update vertical line positions
for (let i = 0; i < vlines.length; i++) {
vlines[i].location = redshifted_line_waves[i];
}
// Trigger updates
source.change.emit();
line_source.change.emit();
""")
redshift_slider.js_on_change('value', callback)
# Create the layout and show the plot
layout = column(redshift_slider, plot)
show(layout)
Exercise 4
Play with the above interactive plot and answer the following questions:
Now we’ve introduced two more filters, N540 and N708. At which redshift do you expect to see the strong emission line in the N540 filter? How about the N708 filter? What are those lines?
For a galaxy with \(z=0.08\), does the light in N708 band only from the emission lines (e.g., H\(\alpha\))? Why or why not? How can we only isolate the H\(\alpha\) emission from an N708 image?
The N708 filter covers 6950-7250 A, and the N540 filter covers 5300-5500 A. What is the redshift range where H\(\alpha\) moves into N708 and [OIII] moves into N540? What’s the width of this redshift range? (It would be narrower than the range covered by the broad-band color in Exercise 3.)
What if the Merian team only built the N708 filter (i.e., no N540)? In this case, do we know whether a galaxy is at \(z=0.08\) or at \(z=0.44\) based on the observed N708 flux? Will there be any redshift degeneracy (when you have two different redshifts but the same
i-N708
color)?Let’s move to very high redshift (>3). The Lyman \(\alpha\) line is the transition of the Hydrogen atom from energy level 2 down to the ground state. It has a wavelength of ~1216 A. At which redshift will you see Lyman-alpha emission in the N540 filter? How about the N708 filter?