Fiting a SED

[1]:
import matplotlib.pyplot as plt
import torch
import sedpy
import starduster
from scipy.optimize import minimize

torch.set_num_threads(1)
torch.manual_seed(999);

This tutorial demonstrates how to fit a SED. We start by building the SED model.

[2]:
sed_model = starduster.MultiwavelengthSED.from_builtin()
sed_model.configure(
    pn_gp=starduster.GalaxyParameter(),
    pn_sfh_disk=starduster.CompositeGrid(
        starduster.InterpolatedSFH(), starduster.InterpolatedMH(),
    ),
    pn_sfh_bulge=starduster.CompositeGrid(
        starduster.InterpolatedSFH(), starduster.InterpolatedMH(),
    ),
    flat_input=True,
)

We then create some mock photometric data. The easiest way to ouput filter fluxes is to use the transmission curves given by sedpy.

[3]:
band_names = [
    'galex_FUV', 'galex_NUV',
    'sdss_u0', 'sdss_g0', 'sdss_r0', 'sdss_i0', 'sdss_z0',
    'twomass_J', 'twomass_H', 'twomass_Ks',
    'wise_w1', 'wise_w2', 'wise_w3', 'wise_w4',
    'herschel_pacs_100', 'herschel_pacs_160',
    'herschel_spire_250', 'herschel_spire_350', 'herschel_spire_500'
]
filters = sedpy.observate.load_filters(band_names)
redshift = 0.01
distmod= 0.
sed_model.configure(filters=filters, redshift=redshift, distmod=distmod, ab_mag=True)
# Generate a random SED
params_true = starduster.sample_effective_region(sed_model)
with torch.no_grad():
    mags = sed_model(params_true, return_ph=True)
# We assume 0.1 mag error for every band
mags_err = torch.full_like(mags, 0.1)

The figure below shows the mock SED.

[4]:
fig, ax = plt.subplots(figsize=(10, 5))

lam_pivot = sed_model.lam_pivot.numpy()
plt.errorbar(lam_pivot, mags.numpy(), mags_err.numpy(), fmt='ks')

ax.set_xscale('log')
ax.set_xlabel(r'$\lambda \, [\rm \mu m]$')
ax.set_ylabel(r'$m_{\rm AB}$')
ax.set_xlim(.1, 1e3)
ax.invert_yaxis()
../_images/tutorials_fitting_sed_7_0.png

This next step is to build the posterior distribution. Posterior is callable, which provides a good interface for various optimisers and sampling tools.

[5]:
noise_model = starduster.IndependentNormal(mags, mags_err)
# Create the initial value for the optimiser
eps = 0.1
sampler = lambda n_samp: params_true + eps*(2*torch.rand(n_samp, len(params_true)) - 1)
params_0 = starduster.sample_effective_region(sed_model, sampler=sampler)

The following code uses the Adam optimiser given by PyTorch to fit the SED. This optimiser requires the first-order gradient.

[6]:
posterior = starduster.create_posterior(sed_model, noise_model, mode='torch', negative=True)
params_pred = starduster.optimize(
    posterior, torch.optim.Adam, params_0, n_step=300, lr=1e-2
)
loss: -2.628e+01: 100%|██████████| 300/300 [00:06<00:00, 44.79it/s]

We may also use the Scipy LBFGS optimiser. We can pass the first-order gradient by setting jac=True.

[7]:
posterior = starduster.create_posterior(sed_model, noise_model, mode='numpy_grad', negative=True)
res_bfgs = minimize(
    posterior, params_0.numpy(), bounds=sed_model.bounds, method='L-BFGS-B', jac=True
)
params_bfgs = res_bfgs.x

Additonaly, we can also employ a optimiser that does not require the gradient. The following output mode can also be applied to a MCMC sampler.

[8]:
posterior = starduster.create_posterior(sed_model, noise_model, mode='numpy', negative=True)
res_powell = minimize(
    posterior, params_0.numpy(), bounds=sed_model.bounds, method='Powell'
)
params_powell = res_powell.x

We compare the results obtained by different optimisers as follows. The results are consistent.

[9]:
print("\tTrue\tAdam\tLBFGS\tPowell")
for params in zip(params_true, params_pred, params_bfgs, params_powell):
    print("\t%.2f\t%.2f\t%.2f\t%.2f"%params)
        True    Adam    LBFGS   Powell
        0.60    0.69    0.70    0.70
        0.27    0.31    0.28    0.31
        -0.16   -0.24   -0.17   -0.24
        -0.14   -0.14   -0.14   -0.14
        -0.39   -0.38   -0.39   -0.38
        0.86    0.86    0.86    0.86
        -0.93   -0.93   -0.92   -0.93
        8.44    8.44    8.44    8.44
        0.04    0.06    0.06    0.06
        8.15    8.12    8.12    8.12
        0.00    -0.12   -0.12   -0.13

The fitting result can be seen in the figure below.

[10]:
# Compute the fitting result
with torch.no_grad():
    f_nu = sed_model(params_pred)
    f_ab = -2.5*torch.log10(f_nu) + 8.9
    mags_pred = sed_model(params_pred, return_ph=True)

# Plot the result
fig, ax = plt.subplots(figsize=(10, 5))

plt.errorbar(lam_pivot, mags, mags_err.numpy(), fmt='ks')
plt.plot(lam_pivot, mags_pred, 'ro', markersize=15, markerfacecolor='none')
plt.plot(sed_model.lam, f_ab, 'r--')

ax.set_xscale('log')
ax.set_xlabel(r'$\lambda \, [\rm \mu m]$')
ax.set_ylabel(r'$m_{\rm AB}$')
ax.set_xlim(.1, 1e3)
ax.set_ylim(top=mags.numpy().max() + 1)
ax.invert_yaxis()
../_images/tutorials_fitting_sed_19_0.png