예제 #1
0
파일: test_core.py 프로젝트: bmorris3/kelp
def test_bl(wavelength, temp):
    kelp_test = bl_test(wavelength, temp)
    check = BlackBody(
        temp * u.K, scale=1 * (u.W / (u.m ** 2 * u.nm * u.sr))
    )(wavelength * u.m)

    np.testing.assert_allclose(check.si.value, kelp_test, rtol=1e-4)
예제 #2
0
def test_fit_blackbody(NGC4945_continuum_rest_frame):
    real_spectrum = NGC4945_continuum_rest_frame
    freq_axis = real_spectrum.frequency_axis
    sinthetic_model = BlackBody(1000 * u.K)
    sinthetic_flux = sinthetic_model(freq_axis)

    dispersion = 3.51714285129581
    first_wave = 18940.578099674
    dispersion_type = "LINEAR  "

    spectrum_length = len(real_spectrum.flux)
    spectral_axis = (first_wave +
                     dispersion * np.arange(0, spectrum_length)) * u.AA
    spec1d = su.Spectrum1D(flux=sinthetic_flux, spectral_axis=spectral_axis)
    frequency_axis = spec1d.spectral_axis.to(u.Hz)

    snth_blackbody = NirdustSpectrum(
        header=None,
        z=0,
        spectrum_length=spectrum_length,
        dispersion_key=None,
        first_wavelength=None,
        dispersion_type=dispersion_type,
        spec1d=spec1d,
        frequency_axis=frequency_axis,
    )

    snth_bb_temp = (snth_blackbody.normalize().convert_to_frequency().
                    fit_blackbody(1200).temperature)

    np.testing.assert_almost_equal(snth_bb_temp.value, 1000, decimal=7)
예제 #3
0
 def evaluate_bb_sed(nu, z, T_dt, R_dt, d_L):
     """evaluate the black body SED corresponding to the torus temperature"""
     nu *= 1 + z
     # geometrical factor for a source of size R_dt at distance d_L
     prefactor = np.pi * np.power((R_dt / d_L).to_value(""), 2) * u.sr
     I_nu = BlackBody().evaluate(nu, T_dt, scale=1)
     return (prefactor * nu * I_nu).to("erg cm-2 s-1")
예제 #4
0
    def integrated_blackbody(self, n_theta, n_phi, f=2**-0.5, cython=True):
        """
        Integral of the blackbody function convolved with a filter bandpass.

        Parameters
        ----------
        n_theta : int
            Number of grid points in latitude
        n_phi : int
            Number of grid points in longitude
        f : float
            Greenhouse parameter (typically 1/sqrt(2)).

        Returns
        -------
        interp_bb : function
            Interpolation function for the blackbody map as a function of
            latitude (theta) and longitude (phi)
        """
        from .fast import _integrated_blackbody

        if cython:
            int_bb, func = _integrated_blackbody(self.hotspot_offset,
                                                 self.omega_drag,
                                                 self.alpha,
                                                 self.C_ml,
                                                 self.lmax,
                                                 self.T_s,
                                                 self.a_rs,
                                                 self.rp_a,
                                                 self.A_B,
                                                 n_theta,
                                                 n_phi,
                                                 self.filt.wavelength.to(
                                                     u.m).value,
                                                 self.filt.transmittance,
                                                 f=f)
            return int_bb, func

        else:
            T, theta_grid, phi_grid = self.temperature_map(n_theta,
                                                           n_phi,
                                                           f,
                                                           cython=cython)
            if (T < 0).any():
                return lambda theta, phi: np.inf

            bb_t = BlackBody(temperature=T * u.K)
            int_bb = np.trapz(bb_t(self.filt.wavelength[:, None, None]) *
                              self.filt.transmittance[:, None, None],
                              self.filt.wavelength,
                              axis=0).si.value
            interp_bb = RectBivariateSpline(theta_grid,
                                            phi_grid,
                                            int_bb,
                                            kx=1,
                                            ky=1)
            return int_bb, lambda theta, phi: interp_bb(theta, phi)[0][0]
예제 #5
0
def OptThinMass(S,
                d=1 * u.kpc,
                wav=1 * u.mm,
                kappa=0.0114 * u.cm**2 / u.g,
                T=20 * u.K):
    bb = BlackBody(temperature=T)
    solar = (S * d**2 / kappa / bb(wav) / u.sr).to(u.M_sun)
    earthly = (S * d**2 / kappa / bb(wav) / u.sr).to(u.M_earth)
    return solar, earthly  #returns mass in solar masses and earth masses
예제 #6
0
 def test_call(self, unit):
     B = BlackBody(temperature=300 * u.K,
                   scale=((const.R_sun.to('au') / const.au)**2) *
                   u.Unit(unit))
     w = np.logspace(-0.5, 3) * u.um
     f = B(w) * np.pi * u.sr
     BB = BlackbodySource(300 * u.K)
     test = BB(w, unit=f.unit).value
     assert np.allclose(test, f.value)
예제 #7
0
def blackbody_spectrum(
    temperature: float,
    scale: float,
    redshift: float = None,
    extinction_av: float = None,
    extinction_rv: float = None,
    get_bolometric_flux: bool = False,
):
    """ """
    wavelengths, frequencies = get_wavelengths_and_frequencies()
    scale_lambda = 1 * FLAM / u.sr
    scale_lambda = 1 / scale * FLAM / u.sr
    scale_nu = 1 / scale * FNU / u.sr

    bb_nu = BlackBody(temperature=temperature * u.K, scale=scale_nu)
    flux_nu = bb_nu(wavelengths) * u.sr
    bolometric_flux = bb_nu.bolometric_flux  #.value

    flux_lambda = flux_nu_to_lambda(flux_nu, wavelengths)

    if extinction_av is not None:
        flux_lambda_reddened = apply(
            calzetti00(np.asarray(wavelengths), extinction_av, extinction_rv),
            np.asarray(flux_lambda),
        )
        flux_nu_reddened = flux_lambda_to_nu(flux_lambda_reddened, wavelengths)
        spectrum_reddened = sncosmo_spectral_v13.Spectrum(
            wave=wavelengths, flux=flux_nu_reddened, unit=FNU)
    spectrum_unreddened = sncosmo_spectral_v13.Spectrum(wave=wavelengths,
                                                        flux=flux_nu,
                                                        unit=FNU)

    if redshift is not None:
        if extinction_av is not None:
            spectrum_reddened.z = 0
            spectrum_reddened_redshifted = spectrum_reddened.redshifted_to(
                redshift,
                cosmo=cosmo,
            )
            outspectrum = spectrum_reddened_redshifted
        else:
            spectrum_unreddened.z = 0
            spectrum_unreddened_redshifted = spectrum_unreddened.redshifted_to(
                redshift, cosmo=cosmo)
            outspectrum = spectrum_unreddened_redshifted

    else:
        if extinction_av is not None:
            outspectrum = spectrum_reddened
        else:
            outspectrum = spectrum_unreddened

    if get_bolometric_flux:
        return outspectrum, bolometric_flux
    else:
        return outspectrum
예제 #8
0
def test_blackbody(temperature):
    wngrid = np.linspace(200, 30000, 200)

    bb = black_body(wngrid, temperature)/np.pi

    bb_as = BlackBody(temperature=temperature * u.K)
    expect_flux = bb_as(wngrid * u.k).to(u.W/u.m**2/u.micron/u.sr,
                                         equivalencies=u.spectral_density(
                                                     wngrid*u.k))

    assert bb == pytest.approx(expect_flux.value, rel=1e-3)
예제 #9
0
    def evaluate_multi_T_bb_sed(nu, z, M_BH, m_dot, R_in, R_out, d_L, mu_s=1):
        r"""Evaluate a multi-temperature black body SED in the case of the SS Disk.
        The SED is calculated for an observer far away from the disk (:math:`d_L \gg R`) 
        with the following:

        .. math::
            \nu F_{\nu} \approx \mu_s \, \nu \frac{2 \pi}{d_L^2} 
            \int_{R_{\rm in}}^{R_{\rm out}}{\rm d}R \, R \, I_{\nu}(T(R)),

        where :math:`I_{\nu}` is Planck's law, :math:`R` the radial coordinate 
        along the disk, and :math:`d_L` the luminosity distance. :math:`\mu_s` 
        is the cosine of the angle between the disk axis and the observer's line of sight.

        Parameters
        ----------
        nu : :class:`~astropy.units.Quantity`
            array of frequencies, in Hz, to compute the sed 
            **note** these are observed frequencies (observer frame)
        z : float
            redshift of the source
        M_BH : :class:`~astropy.units.Quantity`
            Black Hole mass    
        m_dot : float
            mass accretion rate
        R_in : :class:`~astropy.units.Quantity`
            inner disk radius
        R_out : :class:`~astropy.units.Quantity`
            outer disk radius
        d_L : :class:`~astropy.units.Quantity` 
            luminosity of the source
        mu_s : float
            cosine of the angle between the observer line of sight and the disk axis
        """
        # correct for redshift
        nu *= 1 + z
        # array to integrate R
        R = np.linspace(R_in, R_out, 100)
        _R, _nu = axes_reshaper(R, nu)
        _T = SSDisk.evaluate_T(_R, M_BH, m_dot, R_in)
        _I_nu = BlackBody().evaluate(_nu, _T, scale=1)
        integrand = _R / np.power(d_L, 2) * _I_nu * u.sr
        F_nu = 2 * np.pi * np.trapz(integrand, R, axis=0)
        return mu_s * (nu * F_nu).to("erg cm-2 s-1")
예제 #10
0
    def from_blackbody(cls, T_s):
        """
        Return PHOENIX model stellar spectrum for a star with a given effective
        temperature ``T_s`` and surface gravity ``log_g``.

        Parameters
        ----------
        T_s : int
            Effective temperature
        log_g : float
            Surface gravity
        """
        from astropy.modeling.models import BlackBody

        wavelengths = np.linspace(500, 55000, 1000) * u.Angstrom
        bb = np.pi * u.sr * BlackBody(
            T_s * u.K, scale=1 * u.W / u.m**3 / u.sr)(wavelengths)

        return cls(wavelengths, bb)
예제 #11
0
def get_J_CMB():
    #returns the mean intensity for the CMB integrated over min_lam to
    #max_lam (i.e. returns erg/s/cm**2; the same thing as doing 4*sigma
    #T^4)
    min_lam = (1. * u.angstrom).to(u.micron)
    max_lam = (1 * u.cm).to(u.micron)

    wavelengths = np.linspace(min_lam, max_lam, 1.e5)

    # flux = blackbody_lambda(wavelengths,cfg.model.TCMB) # blackbody_lambda deprecated in astropy v4.3
    ## equivalent function provided here https://docs.astropy.org/en/v4.1/modeling/blackbody_deprecated.html#blackbody-functions-deprecated
    bb_lam = BlackBody(cfg.model.TCMB * u.K,
                       scale=1.0 * u.erg / (u.cm**2 * u.AA * u.s * u.sr))
    flux = bb_lam(wavelengths)

    J = np.trapz(flux, wavelengths).to(u.erg / u.s / u.cm**2 / u.sr)
    solid_angle = 4. * np.pi * u.sr
    J = J * solid_angle
    return J
예제 #12
0
def energy_density_absorbed_by_CMB():
    extinction_file = cfg.par.dustdir + cfg.par.dustfile

    mw_df = h5py.File(extinction_file, 'r')
    mw_o = mw_df['optical_properties']
    mw_df_nu = mw_o['nu'] * u.Hz
    mw_df_chi = mw_o['chi'] * u.cm**2 / u.g

    # b_nu = blackbody_nu(mw_df_nu,cfg.model.TCMB) # blackbody_nu deprecated in astropy v4.3
    ## equivalent function provided here https://docs.astropy.org/en/v4.1/modeling/blackbody_deprecated.html#blackbody-functions-deprecated
    bb_nu = BlackBody(cfg.model.TCMB * u.K)
    b_nu = bb_nu(mw_df_nu)

    #energy_density_absorbed = 4pi int b_nu * kappa_nu d_nu since b_nu
    #has units erg/s/cm^2/Hz/str and kappa_nu has units cm^2/g.  this results in units erg/s/g
    steradians = 4 * np.pi * u.sr
    energy_density_absorbed = (steradians * np.trapz(
        (b_nu * mw_df_chi), mw_df_nu)).to(u.erg / u.s / u.g)
    return energy_density_absorbed
예제 #13
0
    def thermal_phase_curve(self, xi, n_theta=20, n_phi=200, f=2 ** -0.5,
                            cython=True, quad=False, check_sorted=True):
        r"""
        Compute the thermal phase curve of the system as a function
        of observer angle ``xi``.

        .. note::

            The ``xi`` axis is assumed to be monotonically increasing when
            ``check_sorted=False``, ``cython=True`` and ``quad=False``.

        Parameters
        ----------
        xi : array-like
            Orbital phase angle
        n_theta : int
            Number of grid points in latitude
        n_phi : int
            Number of grid points in longitude
        f : float
            Greenhouse parameter (typically 1/sqrt(2)).
        cython : bool
            Use Cython implementation of the `integrated_blackbody` function
            (deprecated). Default is True.
        quad : bool
            Use `dblquad` to integrate the temperature map if True,
            else use trapezoidal approximation.
        check_sorted : bool
            Check that the ``xi`` values are sorted before passing to cython
            (carefully turning this off will speed things up a bit)

        Returns
        -------
        phase_curve : `~kelp.PhaseCurve`
            System fluxes as a function of phase angle :math:`\xi`.
        """
        from .fast import _phase_curve

        rp_rs2 = (self.rp_a * self.a_rs) ** 2

        if quad:
            fluxes = np.zeros(len(xi))

            int_bb, interp_blackbody = self.integrated_blackbody(n_theta, n_phi,
                                                                 f, cython)

            def integrand(phi, theta, xi):
                return (interp_blackbody(theta, phi) * sin(theta) ** 2 *
                        cos(phi + xi))

            bb_ts = BlackBody(temperature=self.T_s * u.K)
            planck_star = np.trapz(self.filt.transmittance *
                                   bb_ts(self.filt.wavelength),
                                   self.filt.wavelength).si.value

            for i in range(len(xi)):
                fluxes[i] = dblquad(integrand, 0, np.pi,
                                    lambda x: -xi[i] - np.pi / 2,
                                    lambda x: -xi[i] + np.pi / 2,
                                    epsrel=100, args=(xi[i],)
                                    )[0] * rp_rs2 / np.pi / planck_star
        else:

            if check_sorted:
                if not np.all(np.diff(xi) >= 0):
                    raise ValueError("xi array must be sorted")

            fluxes = _phase_curve(
                xi.astype(np.float64),
                self.hotspot_offset,
                self.omega_drag,
                self.alpha, self.C_ml, self.lmax,
                self.T_s, self.a_rs, self.rp_a,
                self.A_B, n_theta, n_phi,
                self.filt.wavelength.to(u.m).value,
                self.filt.transmittance,
                f,
                self.stellar_spectrum.wavelength.to(u.m).value,
                self.stellar_spectrum.spectral_flux_density.to(u.W/u.m**3)
            )

        return PhaseCurve(xi, 1e6 * fluxes, channel=self.filt.name)
예제 #14
0
from astropy.io import fits
from astropy.modeling.models import BlackBody, Gaussian1D, Gaussian2D
from astropy.utils import NumpyRNGContext
from astropy import units as u

from gempy.library.fitting import fit_1D

_RANDOM_SEED = 42
debug = False


# Simulate an object spectrum. This black body spectrum is not particularly
# meaningful physically; it's just a way to produce a credible continuum-like
# curve using something other than the functions being fitted:
cont_model = BlackBody(temperature=9500.*u.K, scale=5.e6)

# Some sky-line-like features to be rejected / fitted:
sky_model = (Gaussian1D(amplitude=2000., mean=5577., stddev=50.) +
             Gaussian1D(amplitude=1000., mean=6300., stddev=50.) +
             Gaussian1D(amplitude=300., mean=7914., stddev=50.) +
             Gaussian1D(amplitude=280., mean=8345., stddev=50.) +
             Gaussian1D(amplitude=310., mean=8827., stddev=50.))


class TestFit1D:
    """
    Some tests for gempy.library.fitting.fit_1D with 1-2D data.
    """
    def setup_class(self):
예제 #15
0
def get_delta_obs_given_mstars(m2, m3, m1=1.1, make_plot=0, verbose=1):
    """
    You are given a hierarchical eclipsing binary system.
    Star 1 (TOI 837) is the main source of light.
    Stars 2 and 3 are eclipsing.

    Work out the observed eclipse depth of Star 3 in front of Star 2 in each of
    a number of bandpasses, assuming maximally large eclipses, and blackbodies.
    Ah, and also assuming MIST isochrones.
    """

    # Given the stellar masses, get their effective temperatures and
    # luminosities.
    #
    # At ~50 Myr, M dwarf companion PMS contraction will matter for its
    # parameters. Use
    # data.companion_isochrones.MIST_plus_Baraffe_merged_3.5e+07.csv,
    # which was created during the dmag to companion mass conversion process.
    #
    mstars = nparr([m1, m2, m3])

    icdir = os.path.join(DATADIR, 'companion_isochrones')
    df_ic = pd.read_csv(
        os.path.join(icdir, 'MIST_plus_Baraffe_merged_3.5e+07.csv'))

    teff1 = TEFF
    teff2 = _find_nearest(df_ic, 'teff', m2)
    teff3 = _find_nearest(df_ic, 'teff', m3)

    lum1 = (4 * np.pi * (RSTAR * u.Rsun)**2 * const.sigma_sb *
            (TEFF * u.K)**4).to(u.Lsun).value
    lum2 = _find_nearest(df_ic, 'lum', m2)
    lum3 = _find_nearest(df_ic, 'lum', m3)

    teffs = nparr([teff1, teff2, teff3])
    lums = nparr([lum1, lum2, lum3])

    #
    # initialization. other working bandpasses include Johnson_U, Johnson_V,
    # SDSS_g., and SDSS_z.
    #
    bpdir = os.path.join(DATADIR, 'bandpasses')
    bandpasses = [
        'Bessell_U', 'Bessell_B', 'Bessell_V', 'Cousins_R', 'Cousins_I',
        'TESS', 'Johnson_B'
    ]
    bppaths = [
        glob(os.path.join(bpdir, '*' + bp + '*csv'))[0] for bp in bandpasses
    ]

    wvlen = np.logspace(1, 5, 2000) * u.nm

    #
    # do the calculation
    #

    B_lambda_dict = {}

    for ix, temperature, luminosity in zip(range(len(teffs)), teffs * u.K,
                                           lums * u.Lsun):

        bb = BlackBody(temperature=temperature)

        B_nu_vals = bb(wvlen)

        B_lambda_vals = (B_nu_vals * (const.c / wvlen**2)).to(
            u.erg / u.nm / u.s / u.sr / u.cm**2)

        B_lambda_dict[ix] = B_lambda_vals

    # now get the flux in each bandpass
    F_X_dict = {}
    F_dict = {}
    M_X_dict = {}
    M_dict = {}
    T_dict = {}
    L_X_dict = {}
    for bp, bppath in zip(bandpasses, bppaths):

        bpdf = pd.read_csv(bppath)
        if 'nm' in bpdf:
            pass
        else:
            bpdf['nm'] = bpdf['angstrom'] / 10

        bp_wvlen = nparr(bpdf['nm'])
        T_lambda = nparr(bpdf.Transmission)
        if np.nanmax(T_lambda) > 1.1:
            if np.nanmax(T_lambda) < 100:
                T_lambda /= 100  # unit convert
            else:
                raise NotImplementedError

        eps = 1e-6
        if not np.all(np.diff(bp_wvlen) > eps):
            raise NotImplementedError

        interp_fn = interp1d(bp_wvlen,
                             T_lambda,
                             bounds_error=False,
                             fill_value=0,
                             kind='quadratic')

        T_lambda_interp = interp_fn(wvlen)
        T_dict[bp] = T_lambda_interp

        F_X_dict[bp] = {}
        M_X_dict[bp] = {}
        L_X_dict[bp] = {}

        #
        # for each star, calculate erg/s in bandpass
        # NOTE: the quantity of interest is in fact counts/sec.
        # (this is probably a small consideration, but could still be worth
        # checking)
        #

        rstars = []

        for ix, temperature, luminosity in zip(range(len(teffs)), teffs * u.K,
                                               lums * u.Lsun):

            # flux (erg/s/cm^2) in bandpass
            _F_X = (4 * np.pi * u.sr *
                    trapz(B_lambda_dict[ix] * T_lambda_interp, wvlen))

            F_X_dict[bp][ix] = _F_X

            # bolometric flux, according to the blackbody function
            _F_bol = (4 * np.pi * u.sr * trapz(
                B_lambda_dict[ix] * np.ones_like(T_lambda_interp), wvlen))

            # stefan-boltzman law to get rstar from the isochronal temp/lum.
            rstar = np.sqrt(luminosity /
                            (4 * np.pi * const.sigma_sb * temperature**4))
            rstars.append(rstar.to(u.Rsun).value)

            # erg/s in bandpass
            _L_X = _F_X * 4 * np.pi * rstar**2

            L_X_dict[bp][ix] = _L_X.cgs

            if DEBUG:
                print(42 * '-')
                print(f'{_F_X:.2e}, {_F_bol:.2e}, {L_X_dict[bp][ix]:.2e}')

            if ix not in F_dict.keys():
                F_dict[ix] = _F_bol

            # get bolometric magnitude of the star, in the bandpass, as a
            # sanity check
            M_bol_sun = 4.83
            M_bol_star = (-5 / 2 * np.log10(luminosity / (1 * u.Lsun)) +
                          M_bol_sun)
            M_X = M_bol_star - 5 / 2 * np.log10(F_X_dict[bp][ix] / F_dict[ix])

            if ix not in M_dict.keys():
                M_dict[ix] = M_bol_star.value

            M_X_dict[bp][ix] = M_X.value

    delta_obs_dict = {}
    for k in L_X_dict.keys():

        # out of eclipse
        L_ooe = L_X_dict[k][0] + L_X_dict[k][1] + L_X_dict[k][2]

        # in eclipse. assume maximal depth
        f = (rstars[2] / rstars[1])**2
        L_ie = L_X_dict[k][0] + L_X_dict[k][2] + (1 - f) * L_X_dict[k][1]

        # assume the tertiary is eclipsing the secondary.
        delta_obs_dict[k] = ((L_ooe - L_ie) / L_ooe).value

    if verbose:
        for k in delta_obs_dict.keys():
            print(f'{k}: {delta_obs_dict[k]:.2e}')

    if make_plot:
        #
        # plot the result
        #
        from astropy.visualization import quantity_support

        plt.close('all')
        linestyles = ['-', '-', '--']
        f, ax = plt.subplots(figsize=(4, 3))
        with quantity_support():
            for ix in range(3):
                l = f'{teffs[ix]:d} K, {mstars[ix]:.3f} M$_\odot$'
                ax.plot(wvlen, B_lambda_dict[ix], ls=linestyles[ix], label=l)
        ax.set_yscale('log')
        ax.set_ylim(
            [1e-3, 10 * np.nanmax(np.array(list(B_lambda_dict.values())))])
        ax.set_xlabel('Wavelength [nm]')
        ax.legend(loc='best', fontsize='xx-small')

        ax.set_ylabel(
            '$B_\lambda$ [erg nm$^{-1}$ s$^{-1}$ sr$^{-1}$ cm$^{-2}$ ]')
        tax = ax.twinx()
        tax.set_ylabel('Transmission [%]')
        for k in T_dict.keys():
            sel = T_dict[k] > 0
            tax.plot(wvlen[sel], 100 * T_dict[k][sel], c='k', lw=0.5)
        tax.set_xscale('log')
        tax.set_ylim([0, 105])

        ax.set_xlim([5e1, 1.1e4])

        f.tight_layout()
        f.savefig(
            '../results/eclipse_depth_color_dependence/blackbody_transmission.png',
            dpi=300)

    return delta_obs_dict
예제 #16
0
 def bb(self):
     from astropy.modeling.models import BlackBody
     return BlackBody(temperature=self.Td*u.K)
예제 #17
0
 def bb(self):
     """
     `astropy.modeling.models.BlackBody` function with ``self.Td`` dust temperature
     """
     from astropy.modeling.models import BlackBody
     return BlackBody(temperature=self.Td * u.K)
예제 #18
0
def abs_mag_in_bandpass(lum, teff, bandpass='******'):
    """
    lum: bolometric luminosity in units of Lsun
    teff: effective temperature in units of K
    bandpass: '******' or '832', nanometers.
    """

    if bandpass not in ['562', '832', 'NIRC2_Kp']:
        raise ValueError

    if bandpass in ['562', '832']:

        bandpassdir = '../data/WASP4_zorro_speckle/filters/'
        bandpasspath = os.path.join(bandpassdir,
                                    'filter_EO_{}.csv'.format(bandpass))

        bpdf = pd.read_csv(bandpasspath, delim_whitespace=True)

        # the actual tabulated values here are bogus at the long wavelength end.
        # obvious from like... physics, assuming detectors are silicon. (confirmed
        # by Howell in priv. comm.)

        width = 100  # nanometers, around the bandpass middle
        sel = np.abs(float(bandpass) - bpdf.nm) < width

        bpdf = bpdf[sel]

    elif bandpass == 'NIRC2_Kp':

        # NIRC2 Kp band filter from
        # http://svo2.cab.inta-csic.es/theory/fps/getdata.php?format=ascii&id=Keck/NIRC2.Kp
        bandpassdir = '../data/WASP4_NIRC2/'
        bandpasspath = os.path.join(bandpassdir, 'Keck_NIRC2.Kp.dat')

        bpdf = pd.read_csv(bandpasspath,
                           delim_whitespace=True,
                           names=['wvlen_angst', 'Transmission'])

        bpdf['nm'] = bpdf.wvlen_angst / 10

    else:
        raise NotImplementedError

    #
    # see /doc/20200121_blackbody_mag_derivn.pdf for relevant discussion of
    # units and where the equations come from.
    #

    from astropy.modeling.models import BlackBody

    M_Xs = []
    for temperature, luminosity in zip(teff * u.K, lum * u.Lsun):

        bb = BlackBody(temperature=temperature)

        wvlen = nparr(bpdf.nm) * u.nm
        B_nu_vals = bb(wvlen)
        B_lambda_vals = B_nu_vals * (const.c / wvlen**2)

        T_lambda = nparr(bpdf.Transmission)

        F_X = 4 * np.pi * u.sr * trapz(B_lambda_vals * T_lambda, wvlen)

        F = const.sigma_sb * temperature**4

        # https://nssdc.gsfc.nasa.gov/planetary/factsheet/sunfact.html
        M_bol_sun = 4.83
        M_bol_star = (-5 / 2 * np.log10(luminosity / (1 * u.Lsun)) + M_bol_sun)

        # bolometric magnitude of the star, in the bandpass!
        M_X = M_bol_star - 5 / 2 * np.log10(F_X / F)

        M_Xs.append(M_X.value)

    return nparr(M_Xs)