Simulate QSO spectra.

The purpose of this notebook is to demonstrate how to simulate QSO spectra using simqso. We also compare the results with the default (PCA-based) QSO template-generating code.


In [1]:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon

In [2]:
from desisim.templates import SIMQSO, QSO

In [3]:
import multiprocessing
nproc = multiprocessing.cpu_count() // 2

In [4]:
plt.style.use('seaborn-talk')
%matplotlib inline

Specify the random seed so the results are reproducible.


In [5]:
seed = 555
rand = np.random.RandomState(seed)

Generate a handful of spectra with z=[2-4] extending into the mid-IR and display them.


In [6]:
nmodel = 9

In [7]:
simqso = SIMQSO(maxwave=4e4)

In [8]:
%time flux, wave, meta = simqso.make_templates(nmodel, seed=seed, zrange=(2, 4))


CPU times: user 10.3 s, sys: 229 ms, total: 10.5 s
Wall time: 10.6 s

In [9]:
meta


Out[9]:
<Table length=9>
OBJTYPESUBTYPETEMPLATEIDSEEDREDSHIFTMAGFLUX_GFLUX_RFLUX_ZFLUX_W1FLUX_W2OIIFLUXHBETAFLUXEWOIIEWHBETAD4000VDISPOIIDOUBLETOIIIHBETAOIIHBETANIIHBETASIIHBETAZMETALAGETEFFLOGGFEH
magnanomaggiesnanomaggiesnanomaggiesnanomaggiesnanomaggieserg / (cm2 s)erg / (cm2 s)AngstromAngstromkm / sdexdexdexdexGyrKm / s2
str10str10int64int64float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32
QSOLYA030830739462.4795321.68272.070542.122783.570057.1821112.4668-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA130830739462.0690121.95341.628121.654332.449713.97177.11293-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA230830739462.2971920.78115.054644.870367.2731217.439731.7074-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA330830739463.6217322.59580.3316770.9155530.9327571.392821.37868-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA430830739463.3062121.94271.188961.670861.515472.140092.66819-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA530830739463.0889421.86051.361411.802261.776672.988184.17789-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA630830739462.0823920.32086.163237.4419313.049633.737457.7084-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA730830739462.9363320.52854.816176.146136.7656510.266316.3077-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA812994525902.9525221.80361.8741.899231.765792.672724.17468-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0

In [10]:
def qaplot_handful(xlim=None, norm=False, logwave=True, loc='upper right'):
    from matplotlib.ticker import FormatStrFormatter
    srt = np.argsort(meta['REDSHIFT'])
    zz = meta['REDSHIFT'][srt].data
    #print(zz, 1215*(1+zz))
    fig, ax1 = plt.subplots(figsize=(14, 9))
    for ii in range(nmodel):
        if norm:
            ax1.plot(wave, flux[srt[ii], :] / 
                     np.interp(6300, wave, flux[srt[ii], :]) + ii*2, alpha=0.75, 
                     label='z={:.2f}'.format(zz[ii]))
        else:
            ax1.plot(wave, flux[srt[ii], :], alpha=0.75, 
                     label='z={:.2f}'.format(zz[ii]))
    if xlim:
        ax1.set_xlim(xlim)
    if logwave:
        ax1.set_xscale('log')

    ax1.set_ylim(5e-2, ax1.get_ylim()[1])
    #ax1.yaxis.set_major_formatter(FormatStrFormatter('%.0f'))
    ax1.set_xlabel('Observed-Frame Wavelength ($\AA$)')
    ax1.legend(loc=loc, ncol=3)
    if norm:
        ax1.set_ylabel('Relative Flux (offset for clarity)')        
    else:
        ax1.set_yscale('log')
        ax1.set_ylabel(r'Flux ($10^{-17}\ erg\ s^{-1}\ cm^{-2}\ \AA^{-1}$)')
    ax1.margins(0.02)

Plot the full wavelength range.


In [11]:
qaplot_handful()


Normalize and zoom into Lyman-alpha and the Lyman-alpha forest.


In [12]:
qaplot_handful((3600, 6100), norm=True, logwave=False, loc='upper left')


Make some templates using the PCA-based QSO() class and compare them.


In [13]:
qso = QSO(minwave=simqso.wave.min(), maxwave=simqso.wave.max())

In [14]:
zin, magin = meta['REDSHIFT'].data, meta['MAG'].data
%time qflux, qwave, qmeta = qso.make_templates(nmodel, seed=seed, redshift=zin, mag=magin, nocolorcuts=True)


CPU times: user 7.23 s, sys: 489 ms, total: 7.72 s
Wall time: 2.63 s

In [15]:
assert(np.all(qwave == wave))
assert(np.all(meta['REDSHIFT'].data == qmeta['REDSHIFT'].data))
assert(np.all(meta['MAG'].data == qmeta['MAG'].data))

In [16]:
def compare_templates(nplot=nmodel, ncol=3, xlim=None):
    """Plot a random sampling of the basis templates."""
    
    if xlim is None:
        xlim = (3600, 8000)
    
    nspec, npix = flux.shape
    nrow = np.ceil(nplot / ncol).astype('int')
    these = np.argsort(meta['REDSHIFT'].data)

    fig, ax = plt.subplots(nrow, ncol, figsize=(4*ncol, 3*nrow), sharey=False, sharex=True)
    for ii, (thisax, indx) in enumerate(zip(ax.flat, these)):
        zz = meta['REDSHIFT'].data[indx]
        mm = meta['MAG'].data[indx]
        thisax.plot(wave, flux[indx, :], label='SIMQSO')
        thisax.plot(qwave, qflux[indx, :], alpha=0.7, label='QSO')
        thisax.set_xlim(xlim)
        ww = (wave > xlim[0]) * (wave < xlim[1])
        ylim = (flux[indx, ww].min(), flux[indx, ww].max())
        thisax.set_ylim((0.1, ylim[1]))
        #thisax.set_xscale('log')
        thisax.set_yscale('log')
        thisax.yaxis.set_major_locator(plt.NullLocator())
        thisax.yaxis.set_minor_locator(plt.NullLocator())
        #if ii < nspec-ncol-1:
        #    thisax.xaxis.set_major_locator(plt.NullLocator())
        #    thisax.xaxis.set_minor_locator(plt.NullLocator())
        thisax.text(0.88, 0.88, 'z={:.2f}\nr={:.2f}'.format(zz, mm),
                    horizontalalignment='center',
                    verticalalignment='center',
                    transform=thisax.transAxes, fontsize=10)
        if ii == 0:
            thisax.legend(loc='upper left')

    #handles, labels = thisax.get_legend_handles_labels()
    #plt.figlegend(handles, labels, loc=(0.89, 0.88))
    fig.subplots_adjust(wspace=0.02, hspace=0.05)

In [17]:
compare_templates()


Now make more spectra with the redshift priors specified.

Turn off the Lyman-alpha forest for speed.


In [18]:
nmoremodel = 500
redshift = rand.uniform(2, 4, nmoremodel)

In [19]:
%time moreflux, morewave, moremeta, moreqsometa = simqso.make_templates(nmoremodel, seed=seed, redshift=redshift, \
                                                                        return_qsometa=True, lyaforest=False)


CPU times: user 1min 29s, sys: 5.68 s, total: 1min 35s
Wall time: 1min 38s

In [20]:
%time moreqflux, _, moreqmeta = qso.make_templates(nmoremodel, seed=seed, redshift=redshift, \
                                                   mag=moremeta['MAG'].data, lyaforest=False)


WARNING:templates.py:1960:make_templates: 12 spectra could not be computed given the input priors!
CPU times: user 7min 6s, sys: 30 s, total: 7min 36s
Wall time: 2min 31s

In [23]:
def qaplot_props():
    bins = nmoremodel//15
    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
    _ = ax1.hist(moremeta['REDSHIFT'], bins=bins)
    ax1.set_xlabel('Redshift')
    ax1.set_ylabel('Number of Galaxies')
    
    _ = ax2.hist(moremeta['MAG'], bins=bins)
    ax2.set_xlabel(r'$r_{\rm DECaLS}$')
    
    _ = ax3.hist(moreqsometa.data['absMag'], bins=bins)
    ax3.set_xlabel(r'$M_{1450}$')
    ax3.set_xlim(ax3.get_xlim()[::-1])
    plt.subplots_adjust(wspace=0.1)

Show the distribution of redshift, apparent magnitude, and absolute magnitude.


In [24]:
qaplot_props()



In [25]:
def qaplot_vsz():
    from matplotlib.ticker import MaxNLocator
    
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
    ax1.scatter(moremeta['REDSHIFT'], moremeta['MAG'], edgecolor='k', alpha=0.9, s=50)
    ax1.set_xlabel('Redshift')
    ax1.set_ylabel(r'$r_{\rm DECaLS}$')
    ax1.yaxis.set_major_locator(MaxNLocator(integer=True))
    
    ax2.scatter(moremeta['REDSHIFT'], moreqsometa.data['absMag'], edgecolor='k', alpha=0.9, s=50)
    ax2.set_ylabel(r'$M_{1450}$')
    ax2.set_xlabel('Redshift')
    ax2.set_ylim(ax2.get_ylim()[::-1])

    plt.subplots_adjust(wspace=0.3)

Show apparent and absolute magnitude vs redshift.


In [26]:
qaplot_vsz()



In [27]:
def flux2colors(cat):
    """Convert DECam/WISE fluxes to magnitudes and colors."""
    colors = dict()
    #with warnings.catch_warnings(): # ignore missing fluxes (e.g., for QSOs)
    #    warnings.simplefilter('ignore')
    colors['g'] = 22.5 - 2.5 * np.log10(cat['FLUX_G'])
    colors['r'] = 22.5 - 2.5 * np.log10(cat['FLUX_R'])
    colors['z'] = 22.5 - 2.5 * np.log10(cat['FLUX_Z'])
    colors['gr'] = colors['g'] - colors['r']
    colors['gz'] = colors['g'] - colors['z']
    colors['rz'] = colors['r'] - colors['z']
    colors['grz'] = 22.5-2.5*np.log10(cat['FLUX_G'] + 0.8 * cat['FLUX_R'] +  0.5 * cat['FLUX_G'] / 2.3)

    with np.errstate(invalid='ignore'):
        colors['W1'] = 22.5 - 2.5 * np.log10(cat['FLUX_W1'])
        colors['W2'] = 22.5 - 2.5 * np.log10(cat['FLUX_W2'])
        colors['W'] = 22.5 - 2.5 * np.log10(0.75 * cat['FLUX_W1'] + 0.25 * cat['FLUX_W2'])
        colors['rW'] = colors['r'] - colors['W']
        colors['W1W2'] = colors['W1'] - colors['W2']
        colors['grzW'] = colors['grz'] - colors['W']

    return colors

In [28]:
def qso_colorbox(ax, plottype='gr-rz', verts=None):
    """Draw the QSO selection boxes."""

    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    if plottype == 'gr-rz':
        verts = [(-0.3, 1.3),
                 (1.1, 1.3),
                 (1.1, ylim[0]-0.05),
                 (-0.3, ylim[0]-0.05)
                ]
    
    if plottype == 'r-W1W2':
        verts = None
        ax.axvline(x=22.7, ls='--', lw=2, color='k')
        ax.axhline(y=-0.4, ls='--', lw=2, color='k')
    
    if plottype == 'gz-grzW':
        gzaxis = np.linspace(-0.5, 2.0, 50)
        ax.plot(gzaxis, np.polyval([1.0, -1.0], gzaxis), 
                 ls='--', lw=2, color='k')

    if verts:
        ax.add_patch(Polygon(verts, fill=False, ls='--', lw=2, color='k'))

In [29]:
def qaplot_colorcolor(old=False):
    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14, 4))
    ax1.scatter(colors['rz'], colors['gr'], edgecolor='k', 
                alpha=0.9, s=20, label='SIMQSO')
    if old:
        ax1.scatter(qcolors['rz'], qcolors['gr'], edgecolor='k', 
                    alpha=0.9, s=20, label='QSO')
    ax1.set_xlabel('$r - z$')
    ax1.set_ylabel('$g - r$')
    ax1.set_xlim(-0.5, 1.5)
    ax1.set_ylim(-1.0, 1.8)
    qso_colorbox(ax1, 'gr-rz')
    ax1.legend(loc='upper right', ncol=2)
    
    ax2.scatter(colors['r'], colors['W1W2'], edgecolor='k', alpha=0.9, s=20)
    ax2.set_xlabel('$r$')
    ax2.set_ylabel('$W_{1} - W_{2}$')
    ax2.set_xlim(18, 23.5)
    ax2.set_ylim(-0.5, 1)
    qso_colorbox(ax2, 'r-W1W2')
    
    ax3.scatter(colors['gz'], colors['grzW'], edgecolor='k', alpha=0.9, s=20)
    ax3.set_xlabel('$g - z$')
    ax3.set_ylabel('$grz - W$')
    ax3.set_xlim(-1, 1.5)
    ax3.set_ylim(-1, 1.5)
    qso_colorbox(ax3, 'gz-grzW')


    plt.subplots_adjust(wspace=0.35)

In [30]:
colors = flux2colors(moremeta)
qcolors = flux2colors(moreqmeta)

Plot various color-color diagrams with the DESI/QSO selection boundaries overlaid.


In [31]:
qaplot_colorcolor(old=True)


Demonstrate how to regenerate templates from an input metadata table.


In [32]:
nmodel = 9

In [33]:
seed = 5

In [34]:
simqso = SIMQSO(maxwave=4e4)

In [35]:
%time flux, wave, meta, qmeta = simqso.make_templates(5, seed=seed, return_qsometa=True)


CPU times: user 6.17 s, sys: 219 ms, total: 6.39 s
Wall time: 6.5 s

In [36]:
%time ff, ww, mm, qq = simqso.make_templates(input_qsometa=qmeta, return_qsometa=True)


CPU times: user 3.64 s, sys: 95.8 ms, total: 3.74 s
Wall time: 3.78 s

In [37]:
meta


Out[37]:
<Table length=5>
OBJTYPESUBTYPETEMPLATEIDSEEDREDSHIFTMAGFLUX_GFLUX_RFLUX_ZFLUX_W1FLUX_W2OIIFLUXHBETAFLUXEWOIIEWHBETAD4000VDISPOIIDOUBLETOIIIHBETAOIIHBETANIIHBETASIIHBETAZMETALAGETEFFLOGGFEH
magnanomaggiesnanomaggiesnanomaggiesnanomaggiesnanomaggieserg / (cm2 s)erg / (cm2 s)AngstromAngstromkm / sdexdexdexdexGyrKm / s2
str10str10int64int64float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32
QSOLYA09534534110.78259421.30042.887083.018963.8331814.827922.9371-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA19534534113.0845422.0331.336461.537471.289011.476922.16931-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA22369968141.459322.63520.7104310.8829310.8054562.249234.2554-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA39534534111.0540822.60040.6858140.9116641.022352.324193.58663-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA49534534113.5797822.36630.4818491.131080.9326470.9404141.03353-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0

In [38]:
mm


Out[38]:
<Table length=5>
OBJTYPESUBTYPETEMPLATEIDSEEDREDSHIFTMAGFLUX_GFLUX_RFLUX_ZFLUX_W1FLUX_W2OIIFLUXHBETAFLUXEWOIIEWHBETAD4000VDISPOIIDOUBLETOIIIHBETAOIIHBETANIIHBETASIIHBETAZMETALAGETEFFLOGGFEH
magnanomaggiesnanomaggiesnanomaggiesnanomaggiesnanomaggieserg / (cm2 s)erg / (cm2 s)AngstromAngstromkm / sdexdexdexdexGyrKm / s2
str10str10int64int64float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32float32
QSOLYA09534534110.78259421.30042.887083.018963.8331814.827922.9371-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA19534534113.0845422.03531.333641.534171.286091.473582.1644-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA29534534111.459322.63550.7105290.8826510.8062472.248874.2547-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA39534534111.0540822.60050.685730.9115531.022232.32393.58619-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0
QSOLYA49534534113.5797822.36770.4811951.129590.9313790.939131.03212-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0-1.0

In [ ]: