Basic simulation

The grizli extraction and fitting code can work as a spectrum simulator, indeed spectrum "simulation" goes hand-in-hand with fitting. Though here we consider "simulation" in the sense of predicting what dispersed spectra would look like for a known direct image and assumed target spectrum in the case of, e.g., observation planning.


In [1]:
# for notebook
%matplotlib inline

In [2]:
import os
import matplotlib as mpl    
import matplotlib.pyplot as plt    
import matplotlib.gridspec
#mpl.rcParams['figure.figsize'] = (10.0, 6.18)
mpl.rcParams['figure.figsize'] = (8.0, 4.94)

import numpy as np

import astropy.io.fits as pyfits

import grizli

In [3]:
# Files downloaded in examples/Grizli\ Demo.ipynb and put in /tmp/
os.chdir('/tmp/')

## Initialize the FLT object with just a *direct* image.
sim_g141 = grizli.model.GrismFLT(grism_file='', direct_file='ibhj34h6q_flt.fits', pad=100)


No TEAL-based tasks available for this package!

In [4]:
## Make a simple detection catalog + segmentation image with photutils.
sim_g141.photutils_detection(detect_thresh=2, grow_seg=5, gauss_fwhm=2., 
                             verbose=True, save_detection=True)


ibhj34h6q_flt: photutils.detect_sources (detect_thresh=2.0, grow_seg=5, gauss_fwhm=2.0, ZP=26.5)
ibhj34h6q_flt: photutils.source_properties
Rename column: xcentroid -> x_flt
Rename column: dec_icrs_centroid -> dec
Rename column: ycentroid -> y_flt
Rename column: ra_icrs_centroid -> ra
ibhj34h6q_flt: photutils.source_properties - 175 objects
ibhj34h6q_flt: save ibhj34h6q_flt.detect_seg.fits, ibhj34h6q_flt.detect.cat
Out[4]:
True

In [5]:
## Compute the full grism model.
#sim_g141.compute_full_model(compute_beams=['A','B','C','D'], verbose=False)

keep = sim_g141.catalog['mag'] < 26
c = sim_g141.catalog

sim_g141.compute_full_model(ids=c['id'][keep], mags=c['mag'][keep])

# for id_i, mag_i in zip(sim_g141.catalog['id'][keep], sim_g141.catalog['mag'][keep]):
#     sim_g141.compute_model_orders(id=id_i, compute_size=True, mag=mag_i)

In [6]:
# Compare to the actual G141 exposure
g141 = pyfits.open('ibhj34h8q_flt.fits')    
fig = plt.figure()

ax = fig.add_subplot(131)
ax.imshow(g141['SCI'].data, interpolation='Nearest', 
           origin='lower', vmin=-0.01, vmax=0.1, cmap='gray_r')

ax = fig.add_subplot(132)
ax.imshow(sim_g141.model, interpolation='Nearest', 
           origin='lower', vmin=-0.01, vmax=0.1, cmap='gray_r')

ax = fig.add_subplot(133)
ax.imshow(g141['SCI'].data - sim_g141.model[sim_g141.pad:-sim_g141.pad, sim_g141.pad:-sim_g141.pad], 
          interpolation='Nearest', origin='lower', vmin=-0.01, vmax=0.1, cmap='gray_r')

for ax in fig.axes[1:]:
    ax.set_yticklabels([])
    
fig.tight_layout()



In [7]:
## Now simulate a g102 spectrum
sim_g102 = grizli.model.GrismFLT(grism_file='', direct_file='ibhj34h6q_flt.fits', 
                                 force_grism='G102', pad=100)

In [8]:
# Load segmentation image and catalog we just made
sim_g102.load_photutils_detection()
#sim_g102.compute_full_model(compute_beams=['A','B','C','D'], verbose=False)
keep = sim_g102.catalog['mag'] < 26
for id_i, mag_i in zip(sim_g102.catalog['id'][keep], sim_g102.catalog['mag'][keep]):
    sim_g102.compute_model_orders(id=id_i, compute_size=True, mag=mag_i)

In [9]:
## Consider an object near the center of the image
dr = np.sqrt((sim_g141.catalog['x_flt']-sim_g141.pad-579)**2+(sim_g141.catalog['y_flt']-sim_g141.pad-522)**2)    
ix = np.argmin(dr)
id = sim_g141.catalog['id'][ix]
x0 = sim_g141.catalog['x_flt'][ix]+1
y0 = sim_g141.catalog['y_flt'][ix]+1
print('id=%d, (x,y)=(%.1f, %.1f)' %(id, x0, y0))


id=88, (x,y)=(679.9, 622.3)

In [10]:
## Spectrum cutouts
beam_g102 = grizli.model.BeamCutout(sim_g102, sim_g102.object_dispersers[id]['A'])
beam_g141 = grizli.model.BeamCutout(sim_g141, sim_g141.object_dispersers[id]['A']) 
# beam_g141 = grizli.model.BeamCutout(id=id, x=x0, y=y0, cutout_dimensions=[18,18], 
#                                     conf=sim_g141.conf, GrismFLT=sim_g141)


/Users/brammer/.local/lib/python3.5/site-packages/grizli/model.py:2480: RuntimeWarning: overflow encountered in true_divide
  self.ivar = 1/self.grism.data['ERR']**2
/Users/brammer/.local/lib/python3.5/site-packages/grizli/model.py:2507: RuntimeWarning: invalid value encountered in multiply
  contam_mask = ((self.contam*np.sqrt(self.ivar) > contam_sn_mask[0]) &
/Users/brammer/.local/lib/python3.5/site-packages/grizli/model.py:2507: RuntimeWarning: invalid value encountered in greater
  contam_mask = ((self.contam*np.sqrt(self.ivar) > contam_sn_mask[0]) &
/Users/brammer/.local/lib/python3.5/site-packages/grizli/model.py:2508: RuntimeWarning: invalid value encountered in multiply
  (self.model*np.sqrt(self.ivar) < contam_sn_mask[1]))
/Users/brammer/.local/lib/python3.5/site-packages/grizli/model.py:2508: RuntimeWarning: invalid value encountered in less
  (self.model*np.sqrt(self.ivar) < contam_sn_mask[1]))

In [11]:
## Contamination coutouts of spectra around the object of interest
# for beam, sim in zip([beam_g102, beam_g141], [sim_g102, sim_g141]):
#     # flat-spectrum model
#     beam.compute_model(beam.thumb, id=beam.id)  
#     # flat-spectrum model already in sim.model, take it out for the contam cutout
#     beam.contam = beam.get_cutout(sim.model) - beam.model 
plt.imshow(beam_g141.contam, interpolation='Nearest', 
           origin='lower', vmin=-0.008, vmax=0.08, cmap='gray_r')


Out[11]:
<matplotlib.image.AxesImage at 0x1400f37f0>

In [12]:
## Example spectrum, low metallicity star-forming galaxy with strong lines
## after the object described by Erb et al. (2010)
spectrum_file = os.path.join(os.path.dirname(grizli.__file__), 'data/erb2010.dat')
erb = np.loadtxt(spectrum_file, unpack=True)
z = 1.0 # test redshift

# show the spectrum and grism passbands
plt.plot(erb[0]*(1+z)/1.e4, erb[1], label='Erb+2010 spectrum', color='0.4')
plt.semilogy()
plt.xlim(0.75,1.75); plt.ylim(4e-4*erb[1].max(),0.5*erb[1].max())
plt.xlabel(r'$\lambda$'); plt.ylabel(r'[arbitrary]')
plt.plot(beam_g102.beam.lam/1.e4, beam_g102.beam.sensitivity, label='G102')
plt.plot(beam_g141.beam.lam/1.e4, beam_g141.beam.sensitivity, label='G141')
plt.legend()


Out[12]:
<matplotlib.legend.Legend at 0x140283668>
/Users/brammer/anaconda3/lib/python3.5/site-packages/matplotlib/font_manager.py:1288: UserWarning: findfont: Font family ['Courier'] not found. Falling back to Bitstream Vera Sans
  (prop.get_family(), self.defaultFamily[fontext]))

In [13]:
## Use PySynphot to adjust the flux normalization of the models
import pysynphot as S
spec = S.ArraySpectrum(erb[0], erb[1], fluxunits='flam')
# Normalize to AB = 24 in WFC3/IR F160W
ABnorm = 24.0
spec = spec.redshift(z).renorm(ABnorm, 'ABMag', S.ObsBandpass('wfc3,ir,f160w'))
spec.convert('flam') # why is above changing units?

# spec.flux now has units of erg / s / cm2 / A
plt.plot(spec.wave/1.e4, spec.flux)
plt.semilogy()
plt.xlim(0.75,1.75); plt.ylim(4e-4*spec.flux.max(),0.5*spec.flux.max())
plt.xlabel(r'$\lambda$'); plt.ylabel(r'$f_\lambda$ [erg / s / cm$^2$ / $\AA$]')


Out[13]:
<matplotlib.text.Text at 0x1407b9d30>
/Users/brammer/anaconda3/lib/python3.5/site-packages/matplotlib/font_manager.py:1288: UserWarning: findfont: Font family ['Courier'] not found. Falling back to Bitstream Vera Sans
  (prop.get_family(), self.defaultFamily[fontext]))

In [14]:
## Compute the models
# The attribute `beam_g102.total_flux` forces the dispersed spectrum to have the 
# same units as the input model, which we normalized above, integrated over the object
# morphology. 
# Without specifying the normalization term in the denominator as below, the `compute_model` 
# function implicitly assumes that the 1D input model spectrum is normalized to have a 
# flux density of unity integrated over the direct image bandpass.

## OLD
# beam_g102.compute_model(beam_g102.thumb, id=id, 
#                         xspec=spec.wave, yspec=spec.flux/beam_g102.total_flux)    
# beam_g141.compute_model(beam_g141.thumb, id=id, 
#                         xspec=spec.wave, yspec=spec.flux/beam_g141.total_flux)

beam_g102.beam.compute_model(spectrum_1d=[spec.wave, spec.flux/beam_g102.beam.total_flux])    
beam_g141.beam.compute_model(spectrum_1d=[spec.wave, spec.flux/beam_g141.beam.total_flux])


Out[14]:
True

In [15]:
## Test that the computed models have the same total countrate as computed by PySynphot
obs_g102 = S.Observation(spec, S.ObsBandpass('wfc3,ir,g102'))
obs_g141 = S.Observation(spec, S.ObsBandpass('wfc3,ir,g141'))
print('Total spectrum countrates across the bandpass (e-/s)')
print('G102:  grizli = %.2f, pysynphot = %.2f, ratio = %.3f' %(beam_g102.model.sum(), 
                        obs_g102.countrate(), beam_g102.model.sum()/obs_g102.countrate()))
print('G141:  grizli = %.2f, pysynphot = %.2f, ratio = %.3f' %(beam_g141.model.sum(), 
                        obs_g141.countrate(), beam_g141.model.sum()/obs_g141.countrate()))

# The two agree within a few percent with this test, the difference appears to come from
# some inconsistency between the PySynphot throughput table and the aXe sensitivity curves, 
# since the latter must be multiplied by a factor of delta(wave)-per-pixel to get flux 
# density units


Total spectrum countrates across the bandpass (e-/s)
G102:  grizli = 10.22, pysynphot = 10.00, ratio = 1.023
G141:  grizli = 13.60, pysynphot = 13.40, ratio = 1.015

In [16]:
## Show the 2D models
fig = plt.figure()

# G102
ax = fig.add_subplot(211)
ax.set_title('G102')
ax.imshow(beam_g102.model, interpolation='Nearest', 
           origin='lower', vmin=-0.008, vmax=0.08, cmap='gray_r')

# helper functions for 2D spectrum labels
beam_g102.beam.twod_axis_labels(limits=[0.7,1.25,0.1], wscale=1.e4, mpl_axis=ax)
beam_g102.beam.twod_xlim(0.75,1.18, wscale=1.e4, mpl_axis=ax)

# G141
ax = fig.add_subplot(212)
ax.set_title('G141')
ax.imshow(beam_g141.model, interpolation='Nearest', 
           origin='lower', vmin=-0.008, vmax=0.08, cmap='gray_r')

# helper functions for 2D spectrum labels
beam_g141.beam.twod_axis_labels(limits=[1.0,1.8,0.1], wscale=1.e4, mpl_axis=ax)
beam_g141.beam.twod_xlim(1.03,1.75, wscale=1.e4, mpl_axis=ax)
ax.set_xlabel(r'$\lambda$')


Out[16]:
<matplotlib.text.Text at 0x1409f4eb8>
/Users/brammer/anaconda3/lib/python3.5/site-packages/matplotlib/font_manager.py:1288: UserWarning: findfont: Font family ['Courier'] not found. Falling back to Bitstream Vera Sans
  (prop.get_family(), self.defaultFamily[fontext]))

In [17]:
## Plot 1D extractions of the models (error array here is meaningless)
w, f, e = beam_g102.beam.optimal_extract(beam_g102.model, bin=0)
plt.plot(w/1.e4, f, label='G102')

w, f, e = beam_g141.beam.optimal_extract(beam_g141.model, bin=0)
plt.plot(w/1.e4, f, label='G141')
    
plt.xlim(0.75,1.75)
plt.legend()


/Users/brammer/.local/lib/python3.5/site-packages/grizli/model.py:480: RuntimeWarning: invalid value encountered in true_divide
  self.optimal_profile = m/m.sum(axis=0)
Out[17]:
<matplotlib.legend.Legend at 0x12535dbe0>

In [18]:
## Simple add (2D) noise to the G141 spectrum
sky_countrate = 1. # e-/s
texp = 1000.       # s
rn = 20.           # readnoise, e-
rms = np.sqrt(sky_countrate*texp+rn)/texp

np.random.seed(1)
twod_noise = np.random.normal(size=beam_g141.model.shape)*rms

In [19]:
### 1D extraction with noise
# get uncertainties correct
beam_g141.ivar = np.ones_like(beam_g141.model)/rms**2

# no noise
w0, f0, e0 = beam_g141.beam.optimal_extract(beam_g141.model, bin=0, ivar=beam_g141.ivar)
plt.plot(w0/1.e4, f0, label='G141', color='green', linewidth=3)

# with noise
w, f, e = beam_g141.beam.optimal_extract(beam_g141.model + twod_noise, bin=0, ivar=beam_g141.ivar)
plt.errorbar(w/1.e4, f, e, label='G141', color='k', marker='o', linestyle='None', alpha=0.5)

# check uncertainties, RMS should be about unity, chi2 should be about 1
msk = np.isfinite(f)
chi2 = np.sum(((f-f0)**2/e**2)[msk])
print('Chi2 = %.1f, dof=%d, Chi2_nu = %.2f, rms=%.3f' %(chi2, msk.sum()-1, chi2/(msk.sum()-1), 
                                                        np.std(((f-f0)/e)[msk])))

# demo with binning
w, f, e = beam_g141.beam.optimal_extract(beam_g141.model + twod_noise, bin=12, ivar=beam_g141.ivar)
plt.fill_between(w/1.e4, f+e, f-e, color='green', alpha=0.2)

plt.plot([1,1.8],[0,0], linestyle='--')
plt.xlim(1.0, 1.75); plt.ylim(-0.3,0.7)
plt.xlabel(r'$\lambda$'); plt.ylabel('flux (e-/s)')


Chi2 = 189.9, dof=183, Chi2_nu = 1.04, rms=1.013
Out[19]:
<matplotlib.text.Text at 0x125586128>
/Users/brammer/anaconda3/lib/python3.5/site-packages/matplotlib/font_manager.py:1288: UserWarning: findfont: Font family ['Courier'] not found. Falling back to Bitstream Vera Sans
  (prop.get_family(), self.defaultFamily[fontext]))

In [20]:
## Show the 2D models
fig = plt.figure()

# G141
ax = fig.add_subplot(211)
ax.set_title('G141')
ax.imshow(beam_g141.model, interpolation='Nearest', 
           origin='lower', vmin=-0.008, vmax=0.08, cmap='gray_r')

# G141, with noise
ax = fig.add_subplot(212)
ax.set_title('G141, with noise')
ax.imshow(beam_g141.model + twod_noise, interpolation='Nearest', 
           origin='lower', vmin=-0.008, vmax=0.08, cmap='gray_r')

# helper functions for 2D spectrum labels
for ax in fig.axes:
    beam_g141.beam.twod_axis_labels(limits=[1.0,1.8,0.1], wscale=1.e4, mpl_axis=ax)
    beam_g141.beam.twod_xlim(1.0,1.75, wscale=1.e4, mpl_axis=ax)
    ax.set_xlabel(r'$\lambda$')


/Users/brammer/anaconda3/lib/python3.5/site-packages/matplotlib/font_manager.py:1288: UserWarning: findfont: Font family ['Courier'] not found. Falling back to Bitstream Vera Sans
  (prop.get_family(), self.defaultFamily[fontext]))