The Sed object¶
This example demonstrates the various methods associated with the Sed
class.
Sed
objects can be extracted directly from Grid
objects or created by Galaxy
objects. See tutorials on those objects for more information.
[1]:
import matplotlib.pyplot as plt
import numpy as np
from synthesizer.filters import FilterCollection
from synthesizer.grid import Grid
from synthesizer.igm import Madau96
from synthesizer.sed import Sed, plot_spectra_as_rainbow
from unyt import Angstrom, eV, um
Let’s begin by initialising a grid:
[2]:
grid_dir = "../../tests/test_grid/"
grid_name = "test_grid"
grid = Grid(grid_name, grid_dir=grid_dir)
grid_dir = "../../tests/test_grid/"
grid_name = "test_grid"
grid = Grid(grid_name, grid_dir=grid_dir)
Next, let’s define a target log10age and metallicity and use the built-in Grid
method to get the grid point and then extract the spectrum for that grid point.
[3]:
log10age = 6.0 # log10(age/yr)
metallicity = 0.01
spectra_id = "incident"
grid_point = grid.get_grid_point((log10age, metallicity))
sed = grid.get_spectra(grid_point, spectra_id=spectra_id)
sed.lnu *= 1e8 # make the SED bigger
Like other synthesizer
objects, we get some basic information about the Sed
object by using the print
command:
[4]:
print(sed)
----------
SUMMARY OF SED
Number of wavelength points: 9244
Wavelength range: [0.00 Å, 299293000000.00 Å]
log10(Peak luminosity/erg/(Hz*s)): 29.49
log10(Bolometric luminosity/erg/s):36.84609875911521----------
Sed
objects contain a wavelength grid and luminosity in the lam
and lnu
attributes. Both come with units making them easy to convert:
[5]:
print(sed.lam)
print(sed.lnu)
[1.29662e-04 1.33601e-04 1.37660e-04 ... 2.97304e+11 2.98297e+11
2.99293e+11] Å
[0. 0. 0. ... 0. 0. 0.] erg/(Hz*s)
These also have more descriptive aliases:
[6]:
print(sed.wavelength)
print(sed.luminosity_nu)
[1.29662e-04 1.33601e-04 1.37660e-04 ... 2.97304e+11 2.98297e+11
2.99293e+11] Å
[0. 0. 0. ... 0. 0. 0.] erg/(Hz*s)
Thus we can easily make a plot:
[7]:
plt.plot(np.log10(sed.lam), np.log10(sed.lnu))
plt.show()
/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/unyt/array.py:1824: RuntimeWarning: divide by zero encountered in log10
out_arr = func(np.asarray(inp), out=out_func, **kwargs)
![_images/sed_13_1.png](_images/sed_13_1.png)
We can also also visualise the spectrum as a rainbow.
[8]:
fig, ax = plot_spectra_as_rainbow(sed)
plt.show()
![_images/sed_15_0.png](_images/sed_15_0.png)
We can also get the luminosity (\(L\)) or spectral luminosity density per \(\AA\) (\(L_{\lambda}\)):
[9]:
print(sed.luminosity)
print(sed.llam)
[0. 0. 0. ... 0. 0. 0.] erg/s
[0. 0. 0. ... 0. 0. 0.] erg/(s*Å)
Scaling Sed
s¶
Sed
objects can be easily scaled via the *
operator. For example,
[10]:
sed3 = sed * 10
sed4 = 10 * sed
plt.plot(np.log10(sed.lam), np.log10(sed.lnu))
plt.plot(np.log10(sed3.lam), np.log10(sed3.lnu))
plt.plot(np.log10(sed4.lam), np.log10(sed4.lnu))
plt.show()
![_images/sed_19_0.png](_images/sed_19_0.png)
Methods¶
get_bolometric_luminosity()¶
This method allows us to calculate the bolometric luminosity of the sed.
[11]:
sed.measure_bolometric_luminosity()
[11]:
unyt_quantity(7.01614828e+44, 'erg/s')
By default any spectra integration will use a trapezoid method. It’s also possible to use the simpson rule.
[12]:
sed.measure_bolometric_luminosity(integration_method="simps")
[12]:
unyt_quantity(6.99398441e+44, 'erg/s')
You can also get the luminosity or lnu
in a particular window by passing the wavelengths defining the limits of the window.
[13]:
sed.measure_window_luminosity((1400.0 * Angstrom, 1600.0 * Angstrom))
[13]:
unyt_quantity(4.0480619e+43, 'erg/s')
Notice how units were provided with this window. These are required but also allow you to use an arbitrary unit system.
[14]:
sed.measure_window_luminosity((0.14 * um, 0.16 * um))
[14]:
unyt_quantity(4.0480619e+43, 'erg/s')
[15]:
sed.measure_window_lnu((1400.0 * Angstrom, 1600.0 * Angstrom))
[15]:
unyt_quantity(1.51428832e+29, 'erg/(Hz*s)')
Like the bolometric luminosity there are multiple integration methods that can be used for calculating the window. By default it will use the trapezoid method ("trapz"
), but you can also take a simple average.
[16]:
sed.measure_window_lnu(
(1400.0 * Angstrom, 1600.0 * Angstrom), integration_method="average"
)
[16]:
unyt_quantity(1.5142835e+29, 'erg/(Hz*s)')
Or again use Simpson’s method.
[17]:
sed.measure_window_lnu((1400, 1600) * Angstrom, integration_method="simps")
[17]:
unyt_quantity(1.52094846e+29, 'erg/(Hz*s)')
We can also measure a spectral break by providing two windows, e.g.
[18]:
sed.measure_break((3400, 3600) * Angstrom, (4150, 4250) * Angstrom)
[18]:
np.float64(0.8556966386986625)
There are also a few in-built break methods, e.g. measure_Balmer_break()
[19]:
sed.measure_balmer_break()
[19]:
np.float64(0.8556966386986625)
[20]:
sed.measure_d4000()
[20]:
np.float64(0.9014335946523934)
We can also measure absorption line indices:
[21]:
sed.measure_index(
(1500, 1600) * Angstrom, (1400, 1500) * Angstrom, (1600, 1700) * Angstrom
)
[21]:
unyt_quantity(5.80884398, 'Å')
We can also measure the UV spectral slope \(\beta\):
[22]:
sed.measure_beta()
[22]:
np.float64(-2.9472901626578625)
By default this uses a single window and fits the spectrum by a power-law. However, we can also specify two windows as below, in which case the luminosity in each window is calcualted and used to infer a slope:
[23]:
sed.measure_beta(window=(1250, 1750, 2250, 2750))
[23]:
np.float64(-2.9533621846623594)
We can also measure ionising photon production rates for a particular ionisation energy:
[24]:
sed.calculate_ionising_photon_production_rate(
ionisation_energy=13.6 * eV, limit=1000
)
[24]:
unyt_quantity(9.8212605e+54, '1/s')
Observed frame SED¶
To do this we need to provide a cosmology, using an astropy.cosmology
object, a redshift \(z\), and optionally an IGM absorption model.
[25]:
from astropy.cosmology import Planck18 as cosmo
z = 3.0 # redshift
sed.get_fnu(cosmo, z, igm=Madau96) # generate observed frame spectra
[25]:
unyt_array([0., 0., 0., ..., 0., 0., 0.], 'nJy')
[26]:
plt.plot(np.log10(sed.obslam), np.log10(sed.fnu))
plt.show()
/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/unyt/array.py:1824: RuntimeWarning: divide by zero encountered in log10
out_arr = func(np.asarray(inp), out=out_func, **kwargs)
![_images/sed_49_1.png](_images/sed_49_1.png)
We can also plot the observed frame spectra as rainbow.
[27]:
fig, ax = plot_spectra_as_rainbow(sed, use_fnu=True)
plt.show()
![_images/sed_51_0.png](_images/sed_51_0.png)
Photometry¶
Once we have computed the observed frame SED there is a method on an Sed
object that allows us to calculate observed photometry (the same is of course true for rest frame photometry). However, first we need to instantiate a FilterCollection
object.
[28]:
filter_codes = [
f"JWST/NIRCam.{f}"
for f in [
"F070W",
"F090W",
"F115W",
"F150W",
"F200W",
"F277W",
"F356W",
"F444W",
]
] # define a list of filter codes
fc = FilterCollection(filter_codes, new_lam=grid.lam)
[29]:
# Measure observed photometry
fluxes = sed.get_photo_fluxes(fc)
print(fluxes)
------------------------------------------------------------------
| PHOTOMETRY (FLUX) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F070W (λ = 7.04e+03 Å) | 6.76e-30 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F090W (λ = 9.02e+03 Å) | 5.34e-30 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F115W (λ = 1.15e+04 Å) | 3.86e-30 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F150W (λ = 1.50e+04 Å) | 2.77e-30 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F200W (λ = 1.99e+04 Å) | 1.90e-30 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F277W (λ = 2.76e+04 Å) | 1.10e-30 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F356W (λ = 3.57e+04 Å) | 7.10e-31 erg/(Hz*cm**2*s) |
|------------------------------------|---------------------------|
| JWST/NIRCam.F444W (λ = 4.40e+04 Å) | 4.93e-31 erg/(Hz*cm**2*s) |
------------------------------------------------------------------
Multiple SEDs¶
The Sed
object can actually hold an array of seds and the methods should all work fine.
Let’s create an Sed
object with two seds:
[30]:
sed2 = Sed(sed.lam, np.array([sed.lnu, sed.lnu * 2]))
[31]:
sed2.measure_window_lnu((1400, 1600) * Angstrom)
[31]:
unyt_array([1.51428832e+29, 3.02857663e+29], 'erg/(Hz*s)')
[32]:
sed2.measure_window_lnu((1400, 1600) * Angstrom, integration_method="average")
[32]:
unyt_array([1.51428350e+29, 3.02856699e+29], 'erg/(Hz*s)')
[33]:
sed2.measure_beta()
[33]:
array([-2.94729016, -2.94729016])
[34]:
sed2.measure_beta(window=(1250, 1750, 2250, 2750))
[34]:
array([-2.95336218, -2.95336218])
[35]:
sed2.measure_balmer_break()
[35]:
array([0.85569664, 0.85569664])
[36]:
sed2.measure_index(
(1500, 1600) * Angstrom, (1400, 1500) * Angstrom, (1600, 1700) * Angstrom
)
[36]:
unyt_array([5.80884398, 5.80884398], 'Å')
Combining SEDs¶
Sed
s can be combined either via concatenation to produce a single Sed
holding multiple spectra from the combined Sed
s, or by addition to add the spectra contained in two Sed
s.
To concatenate spectra we can use Sed.concat()
.
[37]:
print("Shapes before:", sed._lnu.shape, sed2._lnu.shape)
sed3 = sed2.concat(sed)
print("Combined shape:", sed3._lnu.shape)
Shapes before: (9244,) (2, 9244)
Combined shape: (3, 9244)
Sed.concat
can take an arbitrary number of Sed
objects to combine.
[38]:
sed4 = sed2.concat(sed, sed2, sed3)
print("Combined shape:", sed4._lnu.shape)
Combined shape: (8, 9244)
If we want to add the spectra of 2 Sed
objects we simply apply the +
operator. However, unlike concat
, this will only work for Sed
s with identical shapes.
[39]:
sed5 = sed + sed
plt.plot(np.log10(sed.lam), np.log10(sed.lnu), label="sed")
plt.plot(np.log10(sed5.lam), np.log10(sed5.lnu), label="sed5")
plt.ylim(26, 30)
plt.xlim(2, 5)
plt.xlabel(r"$\rm log_{10}(\lambda/\AA)$")
plt.ylabel(r"$\rm log_{10}(L_{\nu}/erg\ s^{-1}\ Hz^{-1} M_{\odot}^{-1})$")
plt.legend()
plt.show()
plt.close()
![_images/sed_70_0.png](_images/sed_70_0.png)
The Sed
includes a method to resample an sed, e.g. to lower-resolution or to match observations.
[40]:
sed6 = sed.get_resampled_sed(5)
plt.plot(np.log10(sed.lam), np.log10(sed.lnu), label="Original")
plt.plot(np.log10(sed6.lam), np.log10(sed6.lnu), label="Resampled")
plt.xlim(2.2, 3.5)
plt.ylim(27.0, 29.5)
plt.xlabel(r"$\rm log_{10}(\lambda/\AA)$")
plt.ylabel(r"$\rm log_{10}(L_{\nu}/erg\ s^{-1}\ Hz^{-1} M_{\odot}^{-1})$")
plt.legend()
plt.show()
plt.close()
Spectres: new_wavs contains values outside the range in spec_wavs, new_fluxes and new_errs will be filled with the value set in the 'fill' keyword argument.
Spectres: new_wavs contains values outside the range in spec_wavs, new_fluxes and new_errs will be filled with the value set in the 'fill' keyword argument.
![_images/sed_72_1.png](_images/sed_72_1.png)
[41]:
print(
sed.measure_bolometric_luminosity() / sed3.measure_bolometric_luminosity()
)
[1. 0.5 1. ] dimensionless
To apply attenuation to an Sed
you can use the apply_attenuation
method and pass the optical depth. An instance of a dust curve can also be provided but this defaults to a power law with slope = 1 (synthesizer.dust.attenuation.PowerLaw(slope=1)
).
[42]:
sed4_att = sed4.apply_attenuation(tau_v=0.7)
plt.plot(np.log10(sed4.lam), np.log10(sed4.lnu[0, :]), label="Incident")
plt.plot(
np.log10(sed4_att.lam), np.log10(sed4_att.lnu[0, :]), label="Attenuated"
)
plt.xlim(2.2, 3.5)
plt.ylim(26.0, 30.0)
plt.xlabel(r"$\rm log_{10}(\lambda/\AA)$")
plt.ylabel(r"$\rm log_{10}(L_{\nu}/erg\ s^{-1}\ Hz^{-1} M_{\odot}^{-1})$")
plt.legend()
plt.show()
plt.close()
![_images/sed_75_0.png](_images/sed_75_0.png)
The only other argument apply_attenuation
can take is a mask for applying attenuation to speicifc spectra in an Sed
with multiple spectra (like an Sed
containing the spectra for multiple particles for instance.)
[43]:
sed7_att = sed4.apply_attenuation(
tau_v=0.7, mask=np.array([1, 0, 0, 0, 0, 0, 0, 0], dtype=bool)
)
plt.plot(np.log10(sed7_att.lam), np.log10(sed4.lnu[1, :]), label="Incident")
plt.plot(
np.log10(sed7_att.lam), np.log10(sed7_att.lnu[0, :]), label="Attenuated"
)
plt.xlim(2.2, 3.5)
plt.ylim(26.0, 30.0)
plt.xlabel(r"$\rm log_{10}(\lambda/\AA)$")
plt.ylabel(r"$\rm log_{10}(L_{\nu}/erg\ s^{-1}\ Hz^{-1} M_{\odot}^{-1})$")
plt.legend()
plt.show()
plt.close()
![_images/sed_77_0.png](_images/sed_77_0.png)
If you have an attenuated SED, a natural quantity to calculate is the extinction of that spectra (\(A\)). This can be done either at the wavelengths of the Sed
, an arbitrary wavelength/wavelength array, or at commonly used values (1500 and 5500 angstrom) using functions available in the sed
module. Note that these functions return the extinction/attenuation in magnitudes. Below is a demonstration.
[44]:
from synthesizer.sed import (
get_attenuation,
get_attenuation_at_1500,
get_attenuation_at_5500,
get_attenuation_at_lam,
)
from unyt import angstrom, um
# Get an intrinsic spectra
grid_point = grid.get_grid_point((7, 0.01))
int_sed = grid.get_spectra(grid_point, spectra_id="incident")
# Get the attenuated spectra
att_sed = int_sed.apply_attenuation(tau_v=0.7)
# Get attenuation at sed.lam
attenuation = get_attenuation(int_sed, att_sed)
print(attenuation[~np.isnan(attenuation)])
# Get attenuation at 5 microns
att_at_5 = get_attenuation_at_lam(5 * um, int_sed, att_sed)
print(att_at_5)
# Get attenuation at an arbitrary range of wavelengths
att_at_range = get_attenuation_at_lam(
np.linspace(5000, 10000, 5) * angstrom, int_sed, att_sed
)
print(att_at_range)
# Get attenuation at 1500 angstrom
att_at_1500 = get_attenuation_at_1500(int_sed, att_sed)
print(att_at_1500)
# Get attenuation at 5500 angstrom
att_at_5500 = get_attenuation_at_5500(int_sed, att_sed)
print(att_at_5500)
[ inf inf inf ... 0.04221877 0.04207831 0.04193828]
0.08360170415641599
[0.83601817 0.66881427 0.5573455 0.47772428 0.41800898]
2.786725867509981
0.7600168668767144
/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/unyt/array.py:1949: RuntimeWarning: invalid value encountered in divide
out_arr = func(