Synthesizer Crash Course

Getting Started

Synthesizer is a Python package designed to create synthetic astronomical observables from both parametric models and hydrodynamical simulations. It is built to be flexible and modular, allowing users to easily customize and extend its functionality.

You should already have Synthesizer installed if you have followed the installation instructions for Synference. If not, you can run:

pip install cosmos-synthesizer

Synthesizer Grids

Synthesizer relies on pre-computed SPS (Stellar Population Synthesis) model grids to generate synthetic observables. The main documentation for these grids can be found here. Pre-computed grids are available for download for several popular SPS models, including: BPASS, FSPS, BC03, and Maraston, and are stored in HDF5 format.

Additionally, pre-computed grids have been generated for a variety of IMFs, and variants which have been post-processed to include nebular emission using Cloudy are also available.

For the purposes of this crash course, we will use a test grid from BPASS v2.2.1, but the following will work with any of the available grids.

Grids are handled using the synthesizer.Grid class, where the grid_dir argument tells the code where the desired Grid lives.

Here is some code to download the grids using the synthesizer-download command.

import subprocess

subprocess.Popen(["synthesizer-download", "--test-grids", "--dust-grid"])`
[1]:
from synthesizer import Grid

grid = Grid("test_grid")

unyt

Synthesizer uses the unyt package to handle physical units. You can find more information about unyt here.

[2]:
from unyt import Kelvin, Msun, Myr

Instruments and Filters

Synthesizer includes a variety of built-in instruments and filters, which can be found in the documentation. You can also add your own custom filters if needed, and any filter from SVO can be used with the OBSERVATORY/INSTRUMENT.FILTER syntax, e.g. JWST/NIRCam.F356W.

Individual filters are stored in the Filter class, and collection of filters are stored in FilterCollection.

An Instrument stores a FilterCollection as well as other information about the instrument, such as its name and the observatory it belongs to.

For this crash course, we will use a premade instrument which contains all the wide filters for JWST NIRCam, which we can plot.

[3]:
from synthesizer.instruments.filters import Filter

filter = Filter("JWST/NIRCam.F356W")

from synthesizer.instruments import JWSTNIRCamWide

nircam = JWSTNIRCamWide()

nircam.filters.plot_transmission_curves()
[3]:
(<Figure size 500x350 with 1 Axes>,
 <Axes: xlabel='$\\rm \\lambda/\\AA$', ylabel='$\\rm T_{\\lambda}$'>)
../_images/library_gen_synthesizer_crash_course_7_1.png

Creating a Mock Galaxy

Synthesizer has a framework for creating mock galaxies using parametric and particle models. Here we will focus on the parametric models, but you can find more information about the particle models in the documentation.

The framework for creating mock galaxies is built around the Galaxy class. A Galaxy contains a stellar component, which is a Stars instance. The Stars class uses the SFH and metallicity model, as well as the SPS Grid we created earlier, to generate the stellar population of the galaxy.

[4]:
from synthesizer.parametric import Galaxy, Stars

Before we can create a Stars instance, we need to define a star formation history (SFH) and a metallicity distribution. Let’s start with the SFH.

1. Define Star Formation History

Synthesizer includes several built-in star formation history (SFH) models, including commonly used models such as:

  • Constant

  • Exponential

  • Delayed Exponential

  • Lognormal

  • Double Power Law

These parametric models are typically defined by 1-3 parameters, which can be specified when creating the SFH instance. You can also create your own custom SFH models by subclassing the SFH class.

In this example we will use a simple constant SFH, which is defined by two parameters min_age and max_age, which define the age range over which the SFH is constant. We do not define a Star Formation Rate (SFR) or total mass here, as the SFH will be normalized to the total mass we provide when creating the Stars instance.

[5]:
from synthesizer.parametric import SFH

sfh_history = SFH.Constant(min_age=0 * Myr, max_age=100 * Myr)

We can plot the SFH using the plot_sfh method, and see that it is constant between 0 and 100 Myr.

[6]:
sfh_history.plot_sfh(t_range=(0, 2e8))
../_images/library_gen_synthesizer_crash_course_14_0.png

2. Define Metallicity Distributions

As SPS grids are typically defined over both age and metallicity, we also need to define a metallicity distribution for our stellar population. Synthesizer includes several built-in metallicity distribution models, including:

  • Delta Function

  • Gaussian

Here we will use a simple delta function, which is defined by a single parameter metallicity, which defines the metallicity of all stars in the population. The metallicity here is defined as the mass fraction of metals, so a metallicity of 0.02 corresponds (approximately) to solar metallicity.

[7]:
from synthesizer.parametric import ZDist

metal_dist = ZDist.DeltaConstant(metallicity=0.02)

3. Creating the Galaxy

Now that we have defined the SFH and metallicity distribution, we can create a Stars instance. The Stars class takes the age and metallicity arrays of the SPS grid, the SFH and metallicity distribution instances, and the total stellar mass of the population as input.

[8]:
stellar_component = Stars(
    grid.log10age,
    grid.metallicity,
    sf_hist=sfh_history,
    metal_dist=metal_dist,
    initial_mass=1e10 * Msun,
)
/opt/hostedtoolcache/Python/3.10.19/x64/lib/python3.10/site-packages/unyt/array.py:1832: RuntimeWarning: divide by zero encountered in log10
  out_arr = func(np.asarray(inp), out=out_func, **kwargs)

Now we can finally create a Galaxy instance, which takes the Stars instance as input. The Galaxy class can also take additional components, such as gas or a black hole model, but we will not include those in this example. The Galaxy object also takes a redshift parameter, which is used to calculate the luminosity distance and apply cosmological redshifting to the SED.

[9]:
galaxy = Galaxy(stars=stellar_component, redshift=1)

3. Defining your Emission Model

We now have a Galaxy, but we need to define how to generate the SED from its stellar populations. This is the job of an EmissionModel, which takes the galaxy properties (like stellar masses, ages, and metallicities) and combines them with a pre-computed Grid to produce the synthetic SED.

Emission models follow a tree-like structure to accurately model the various components of a galaxy SED, such as:

  • Incident (no nebular emission)

  • Nebular line emission

  • Nebular continuum

  • Full nebular emission (lines + continuum)

  • Intrinsic (nebular + stellar)

When we include modelling of dust attenuation and emission, non-zero escape fractions, and/or AGN emission, the complexity of the emission model increases significantly. Since the possible combinations of models are extensive, Synthesizer provides a comprehensive library of Premade Emission Models which are listed in the documentation.

We encourage you to explore the emission models in the documentation, as they are too numerous to list here. For this example, we will use a TotalEmission model, which is a pre-built model that generates the final combined spectrum, accounting for stellar, nebular, dust attenuation, and thermal dust emission.

For flexibility, our expected emission model components can be set globally on the emission model, or on individual ‘emitters’, such as the Star or Galaxy instances. For example, we can set the escape fraction of ionizing photons to 0.1 for the entire emission model, or we can set it to 0.2 for just the stellar component of the galaxy.

Below we set the escape fraction (\(f_{esc}\)) to 0.1 for the entire emission model, but if we did not set it here, but instead set it on the Galaxy instance, it would override this value.

Before we create our emission model, we need to define our dust attenuation and emission models. Here we use a simple power-law dust attenuation curve, and a single-temperature blackbody for the dust emission. You can find out more information about the wide range of dust models available in the documentation.

Before instantiating our emission model, we first define the components governing the dust physics:

  • Dust Attenuation: We use a simple power-law dust attenuation curve

  • Dust Emission: We define the resulting thermal dust emission using a single-temperature blackbody.

[10]:
from synthesizer.emission_models import Blackbody
from synthesizer.emission_models.attenuation import PowerLaw

dust_curve = PowerLaw(slope=-0.7)
dust_emission_model = Blackbody(temperature=30 * Kelvin)

Now we can create our emission model by supplying the dust components, the SPS grid, and specifying a V-band optical depth (\(\tau_{\rm V}\)) to apply to the emission.

[11]:
from synthesizer.emission_models import TotalEmission

emission_model = TotalEmission(
    grid=grid, dust_curve=dust_curve, tau_v=0.3, dust_emission_model=dust_emission_model
)

We can also plot the emission tree to see the components and complexity of our emission model!

[12]:
emission_model.plot_emission_tree()
../_images/library_gen_synthesizer_crash_course_28_0.png
[12]:
(<Figure size 600x600 with 1 Axes>, <Axes: >)

4. Generate observables

Now that we have a galaxy, an emission model, and an instrument, we can generate our synthetic observables. The easiest observable to generate is the galaxy’s rest-frame SED.

This SED is generated by calling the get_spectra method on the Galaxy instance, passing your configured emission model as the argument. This method calculates the complete, integrated SED and returns it as a specialized Sed object, containing the rest-frame wavelength and spectral luminosity density arrays.

Rest-frame SED

[13]:
galaxy.get_spectra(emission_model=emission_model)
galaxy.get_spectra_combined()
/opt/hostedtoolcache/Python/3.10.19/x64/lib/python3.10/site-packages/unyt/array.py:1832: RuntimeWarning: overflow encountered in exp
  out_arr = func(np.asarray(inp), out=out_func, **kwargs)
/opt/hostedtoolcache/Python/3.10.19/x64/lib/python3.10/site-packages/unyt/array.py:1972: RuntimeWarning: overflow encountered in multiply
  out_arr = func(

In addition, the generated spectra for the root emission model and the child models are stored in the galaxy.stars.spectra dictionary. We can plot the total SED, as well as the individual components, such as the stellar and nebular emission by doing the following:

[14]:
galaxy.plot_spectra(stellar_spectra=True)
[14]:
(<Figure size 350x500 with 1 Axes>,
 <Axes: xlabel='$\\lambda/[\\mathrm{\\AA}]$', ylabel='$L_{\\nu}/[\\mathrm{\\rm{erg} \\ / \\ \\rm{Hz \\cdot \\rm{s}}}]$'>)
../_images/library_gen_synthesizer_crash_course_32_1.png

Observed-frame SED

To convert the rest-frame SED into observed-frame fluxes, you need to define two things: the cosmology and the Intergalactic Medium (IGM) absorption model.

To calculate observed frame fluxes, we need to choose a cosmology. Synthesizer leverages astropy.cosmology for cosmological calculations, allowing you to choose any of the built-in cosmologies (such as the default Plank18), or define your own. For IGM absorption, you must choose a model to account for the attenuation of flux along the line of sight.

Here we will use the default Planck18 cosmology and we choose to use the Inoue2014 IGM model (the only one currently implemented in Synthesizer).

[15]:
from astropy.cosmology import Planck18 as cosmo

galaxy.get_observed_spectra(cosmo=cosmo)

We can plot our observed-frame SED, which includes the effects of cosmological redshifting and IGM absorption:

[16]:
galaxy.plot_observed_spectra(stellar_spectra=True)
[16]:
(<Figure size 350x500 with 1 Axes>,
 <Axes: xlabel='$\\lambda_\\mathrm{obs}/[\\mathrm{\\AA}]$', ylabel='$F_{\\nu}/[\\mathrm{\\rm{nJy}}]$'>)
../_images/library_gen_synthesizer_crash_course_36_1.png

Photometric Fluxes

To obtain observed photometry through specific bands, we need an Instrument object. This allows our fluxes to be calculated using the get_photo_fnu method, which is available on the Galaxy and Stars objects, and returns a PhotometryCollection object containing the fluxes for every filter specified.

[17]:
fluxes = galaxy.stars.get_photo_fnu(filters=nircam.filters)
[18]:
print(fluxes["total"])
-----------------------------------------------------
|                 PHOTOMETRY (FLUX)                 |
|------------------------------------|--------------|
| JWST/NIRCam.F070W (λ = 7.04e+03 Å) | 2.70e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F090W (λ = 9.02e+03 Å) | 3.20e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F115W (λ = 1.15e+04 Å) | 2.45e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F150W (λ = 1.50e+04 Å) | 2.66e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F200W (λ = 1.99e+04 Å) | 3.37e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F277W (λ = 2.76e+04 Å) | 3.69e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F356W (λ = 3.57e+04 Å) | 4.65e+04 nJy |
|------------------------------------|--------------|
| JWST/NIRCam.F444W (λ = 4.40e+04 Å) | 3.25e+04 nJy |
-----------------------------------------------------

Other Calculations

We can calculate other observables, such as emission line fluxes, and equivalent widths, or metadata such as the surviving stellar mass, mass-weighted age or total ionizing luminosity using the appropriate methods of the Galaxy or Stars classes. You can find more information about these methods in the documentation.

Why does this matter?

This core framework, built around grids, galaxy components, and emission models, grants users a high degree of flexibility in generating synthetic observables. By mixing and matching different components, users can construct a vast array of galaxy models tailored to specific astrophysical needs.

This modularity provides the foundation for powerful high-level tools. For instance, the SBI-Fitters library generation tools build directly on this structure to efficiently create the large libraries of synthetic observables needed for simulation-based inference (SBI).

The Synference library generation tools build on this framework to create large libraries of synthetic observables for use in simulation-based inference. You can learn more about library generation in the next section of the documentation.