In this notebook we compute and present data approximating the bandpass of the MEarth observatory. The results are only approximately correct for a variety of reasons, but they should be useful for basic applications. A similar bandpass is presented graphically in Nutzman & Charbonneau (2008) but, to the best of my knowledge, tabulated data are not provided elsewhere.

What we're calculating here is just the combination of the responses of the MEarth CCDs and its filters. The filters are just RG715 glass. RG715 passes long wavelengths, so the red end of the total system response is defined by a combination of the CCD and telluric H2O absorption. The underlying data were provided to me (Peter K. G. Williams) by Jonathan Irwin, and consist of measurements read off of the data sheets for the CCD and the glass. Here I fit splines to these data, so that they can be sampled at arbitrary wavelengths; then I just sample the product of the two functions.

The limitations in this approach should be clear. We're not including the transmission of any other component in the MEarth system, including the atmosphere, which (as mentioned above) affects the red cutoff of the bandpass. The underlying data are approximate at best. MEarth consists of multiple telescopes, each of which will have slightly different characteristics. Less obviously, the MEarth cameras were rebuilt between the 2010–2011 and 2011–2012 seasons, and the response of each telescope before and after this time will differ systematically. Finally, it is important to emphasize that the 2010–2011 season used a completely different optical filter. The bandpass presented here is wrong for 2010–2011 data. See the MEarth DR2 release notes for more information.

The bandpass given here is in terms of response per photon (“quantum efficiency”, “photon counting”), not per unit energy (“equal energy”, “energy integration”). See Casagrande & VandenBerg (2014) for a recent discussion of the distinction. Conversion between the two conventions is easy, but you must be aware of which should be used in different applications.

We begin with some standard Python preambular material:

In [2]:
from __future__ import absolute_import, division
from __future__ import print_function, unicode_literals
import numpy as np, scipy.interpolate as si
from StringIO import StringIO

try:
    # This is my homegrown plotting package, so it's optional:
    import omega as om, omega.ipynb
    have_omega = True
except:
    have_omega = False

Here we set up the spline approximation to the CCD bandpass, using data provided by Jonathan Irwin. Here and below, wavelengths are in Ångström. Since this is a quantum efficiency curve, we see that at 7500 Å, 80% of incoming photons are detected.

In [3]:
ccd_samples = '''
3500 0.20
4000 0.52
4500 0.78
5000 0.90
5500 0.93
6000 0.93
6500 0.91
7000 0.87
7500 0.80
8000 0.70
8500 0.57
9000 0.42
9500 0.28
10000 0.15
10500 0.05
'''

wlen, resp = np.loadtxt (StringIO (ccd_samples)).T
ccd_tck = si.splrep (wlen, resp, s=0.0001)
ccd_model = lambda x: si.splev (x, ccd_tck)
    
if have_omega:
    # Check if the spline approximation looks good.
    x = np.linspace (3300, 10800, 200)
    y = ccd_model (x)
    p = om.quickXY (x, y, 'CCD: Spline')
    p.addXY (wlen, resp, 'CCD: Samples', lines=False)
    p.show ()

And here we approximate the RG715 filter. The spline approximation doesn't work well at the filter edges, so we have to do a bit more work.

In [4]:
rg715_samples = '''
6000 0.0
6800 0.0
6900 0.001
7000 0.04
7100 0.28
7200 0.61
7300 0.80
7400 0.87
7500 0.90
7600 0.91
7700 0.91
7800 0.91
7900 0.91
8000 0.91
8500 0.91
9000 0.91
9500 0.91
10000 0.91
'''

wlen, resp = np.loadtxt (StringIO (rg715_samples)).T
rg_tck = si.splrep (wlen, resp, s=0.0001)

def rg_model (x):
    # This function would return a 1D array for a scalar `x` input,
    # which is a bit funky, but not a problem here.
    x = np.atleast_1d (x)
    y = si.splev (x, rg_tck)
    y[x < 6900] = 0. # note: we're creating a discontinuity here
    y[x > 7600] = 0.91
    return y

if have_omega:
    x = np.linspace (6000, 10000, 200)
    y = rg_model (x)
    p = om.quickXY (x, y, 'RG715: Spline')
    p.addXY (wlen, resp, 'RG715: Samples', lines=False)
    p.show ()

Now we can sample the product of the two. The locations of the sample points were chosen by trial-and-error to give a good approximation of the product and reach just to the filter cutoff on the red and blue end. The location of the red cutoff is a bit arbitrary, because the CCD response data never go quite to zero. The sampling is overdense, given the underyling data, but there's no harm in that.

In [5]:
x = np.linspace (6850, 10900, 64)
y = ccd_model (x) * rg_model (x)
y[:2] = 0. # for sanity, make bandpass edges go to precisely zero.
y[-2:] = 0.

if have_omega:
    om.quickXY (x, y, 'Product').show ()

Let's check that the filter edges look sane. Spline approximations don't deal with discontinuities well, so these are the locations where things are most likely to run into problems. Of course, I tweaked the code above to make sure that everything works out OK, so it should be no surprise that everything is hunky-dory here.

In [6]:
print ('Blue end:')
print ('  '.join ('%8.2f' % q for q in x[:5]))
print ('  '.join ('%8.2f' % q for q in y[:5]))
print ()

print ('Red end:')
print ('  '.join ('%8.2f' % q for q in x[-5:]))
print ('  '.join ('%8.2f' % q for q in y[-5:]))
print ()

print ('Any negative?', np.any (y < 0))
Blue end:
 6850.00   6914.29   6978.57   7042.86   7107.14
    0.00      0.00      0.02      0.11      0.26

Red end:
10642.86  10707.14  10771.43  10835.71  10900.00
    0.02      0.02      0.01      0.00      0.00

Any negative? False

Here's a final table of values suitable for copying and pasting. Recall that wavelengths are in Ångström and that this is a quantum-efficiency curve, not an equal-energy curve. While many response curves are normalized to a peak of unity, I haven’t rescaled since here we have the true quantum efficiency of the filter/CCD system.

In [9]:
print ('# Approximate response function of the MEarth observatory.')
print ('# Telluric absorption not included.')
print ('# Wavelengths in Angstrom. This is a quantum-efficiency, '
       'not equal-energy, curve.')
print ('# Computed from data provided to Peter K. G. Williams by '
       'Jonathan Irwin in 2014.')
print ('# See http://newton.cx/~peter/2014/08/the-bandpass-of-the-'
       'mearth-observatory/ for methodology.')
for i in xrange (x.size):
    print (('%.1f' % x[i]).ljust (10), '%.2f' % y[i])
# Approximate response function of the MEarth observatory.
# Telluric absorption not included.
# Wavelengths in Angstrom. This is a quantum-efficiency, not equal-energy, curve.
# Computed from data provided to Peter K. G. Williams by Jonathan Irwin in 2014.
# See http://newton.cx/~peter/2014/08/the-bandpass-of-the-mearth-observatory/ for methodology.
6850.0     0.00
6914.3     0.00
6978.6     0.02
7042.9     0.11
7107.1     0.26
7171.4     0.44
7235.7     0.58
7300.0     0.66
7364.3     0.70
7428.6     0.72
7492.9     0.72
7557.1     0.71
7621.4     0.71
7685.7     0.70
7750.0     0.69
7814.3     0.67
7878.6     0.66
7942.9     0.65
8007.1     0.64
8071.4     0.62
8135.7     0.61
8200.0     0.59
8264.3     0.58
8328.6     0.56
8392.9     0.55
8457.1     0.53
8521.4     0.51
8585.7     0.50
8650.0     0.48
8714.3     0.46
8778.6     0.44
8842.9     0.43
8907.1     0.41
8971.4     0.39
9035.7     0.37
9100.0     0.36
9164.3     0.34
9228.6     0.32
9292.9     0.31
9357.1     0.29
9421.4     0.27
9485.7     0.26
9550.0     0.24
9614.3     0.22
9678.6     0.21
9742.9     0.19
9807.1     0.18
9871.4     0.17
9935.7     0.15
10000.0    0.14
10064.3    0.12
10128.6    0.11
10192.9    0.10
10257.1    0.09
10321.4    0.07
10385.7    0.06
10450.0    0.05
10514.3    0.04
10578.6    0.03
10642.9    0.02
10707.1    0.02
10771.4    0.01
10835.7    0.00
10900.0    0.00

In some cases, it might be helpful to have a function that can evaluate the response function at an arbitrary wavelength. We can do this with SciPy's interp1d function, which gives us a piecewise linear model. This isn't the most conceptually beautiful, but given the uncertainties inherent in this exercise, it is no more inaccurate than any other approach.

In [8]:
bp_model = si.interp1d (x, y, kind='linear',
                        bounds_error=False, fill_value=0.)

if have_omega:
    mx = np.linspace (6200, 11600, 300)
    my = bp_model (mx)
    p = om.quickXY (mx, my, 'Interpolated model')
    p.addXY (x, y, 'Samples', lines=False)
    p.defaultKeyOverlay.hAlign = 1.
    p.setBounds (ymin=-0.05)
    p.show ()

We see above that the descending portion of the curve is very much oversampled in our table of values. But who cares?