Exemple #1
0
 def _compute(self):
     #  Invert arrays so the first element is the bottom of column
     T = _climlab_to_convect(self.state['Tatm'])
     dom = self.state['Tatm'].domain
     P = _climlab_to_convect(dom.lev.points)
     PH = _climlab_to_convect(dom.lev.bounds)
     Q = _climlab_to_convect(self.state['q'])
     QS = qsat(T, P)
     ND = np.size(T, axis=1)
     NCOL = np.size(T, axis=0)
     NL = ND - 1
     try:
         U = _climlab_to_convect(self.state['U'])
     except:
         U = np.zeros_like(T)
     try:
         V = _climlab_to_convect(self.state['V'])
     except:
         V = np.zeros_like(T)
     NTRA = 1
     TRA = np.zeros((NCOL, ND, NTRA), order='F')  # tracers ignored
     DELT = float(self.timestep)
     CBMF = self.CBMF
     (IFLAG, FT, FQ, FU, FV, FTRA, PRECIP, WD, TPRIME, QPRIME, CBMFnew,
     Tout, Qout, QSout, Uout, Vout, TRAout) = \
         convect(T, Q, QS, U, V, TRA, P, PH, NCOL, ND, NL, NTRA, DELT, self.IPBL, CBMF,
                 CPD, CPV, CL, RV, RD, LV0, G, ROWL, self.MINORIG,
                 self.ELCRIT, self.TLCRIT, self.ENTP, self.SIGD, self.SIGS,
                 self.OMTRAIN, self.OMTSNOW, self.COEFFR, self.COEFFS,
                 self.CU, self.BETA, self.DTMAX, self.ALPHA, self.DAMP
                 )
     # If dry adjustment is being used then the tendencies need to be adjusted
     if self.IPBL != 0:
         FT += (Tout - T) / DELT
         FQ += (Qout - Q) / DELT
     tendencies = {
         'Tatm': _convect_to_climlab(FT) * np.ones_like(self.state['Tatm']),
         'q': _convect_to_climlab(FQ) * np.ones_like(self.state['q'])
     }
     if 'Ts' in self.state:
         # for some strange reason self.Ts is breaking tests under Python 3.5 in some configurations
         tendencies['Ts'] = 0. * self.state['Ts']
     if 'U' in self.state:
         tendencies['U'] = _convect_to_climlab(FU) * np.ones_like(
             self.state['U'])
     if 'V' in self.state:
         tendencies['V'] = _convect_to_climlab(FV) * np.ones_like(
             self.state['V'])
     self.CBMF = CBMFnew
     #  Need to convert from mm/day to mm/s or kg/m2/s
     #  Hack to handle single column and multicolumn
     if self.multidim:
         self.precipitation[:, 0] = _convect_to_climlab(
             PRECIP) / const.seconds_per_day
     else:
         self.precipitation[:] = _convect_to_climlab(
             PRECIP) / const.seconds_per_day
     self.IFLAG = IFLAG
     self.relative_humidity[:] = self.q / qsat(self.Tatm, self.lev)
     return tendencies
Exemple #2
0
def moist_amplification_factor(Tkelvin, relative_humidity=0.8):
    '''Compute the moisture amplification factor for the moist diffusivity
    given relative humidity and reference temperature profile.'''
    deltaT = 0.01
    #  slope of saturation specific humidity at 1000 hPa
    dqsdTs = (qsat(Tkelvin + deltaT / 2, 1000.) -
              qsat(Tkelvin - deltaT / 2, 1000.)) / deltaT
    return const.Lhvap / const.cp * relative_humidity * dqsdTs
Exemple #3
0
def test_thermo_domain():
    '''Can we call qsat, etc on a multi-dim climlab state temperature object?'''
    state = climlab.column_state(num_lev = 30, num_lat=3)
    T = state.Tatm
    p = T.domain.lev.points
    thermo.clausius_clapeyron(T)
    thermo.qsat(T, p)
    thermo.pseudoadiabat(T, p)
    thermo.blackbody_emission(T)
Exemple #4
0
def test_thermo_domain():
    '''Can we call qsat, etc on a multi-dim climlab state temperature object?'''
    state = climlab.column_state(num_lev = 30, num_lat=3)
    T = state.Tatm
    p = T.domain.lev.points
    thermo.clausius_clapeyron(T)
    thermo.qsat(T, p)
    thermo.pseudoadiabat(T, p)
    thermo.blackbody_emission(T)
 def _compute(self):
     #  Invert arrays so the first element is the bottom of column
     T = _climlab_to_convect(self.state['Tatm'])
     dom = self.state['Tatm'].domain
     P = _climlab_to_convect(dom.lev.points)
     PH = _climlab_to_convect(dom.lev.bounds)
     Q = _climlab_to_convect(self.state['q'])
     QS = qsat(T,P)
     ND = np.size(T, axis=1)
     NCOL = np.size(T, axis=0)
     NL = ND-1
     try:
         U = _climlab_to_convect(self.state['U'])
     except:
         U = np.zeros_like(T)
     try:
         V = _climlab_to_convect(self.state['V'])
     except:
         V = np.zeros_like(T)
     NTRA = 1
     TRA = np.zeros((NCOL,ND,NTRA), order='F')  # tracers ignored
     DELT = float(self.timestep)
     CBMF = self.CBMF
     (IFLAG, FT, FQ, FU, FV, FTRA, PRECIP, WD, TPRIME, QPRIME, CBMFnew,
     Tout, Qout, QSout, Uout, Vout, TRAout) = \
         convect(T, Q, QS, U, V, TRA, P, PH, NCOL, ND, NL, NTRA, DELT, self.IPBL, CBMF,
                 CPD, CPV, CL, RV, RD, LV0, G, ROWL, self.MINORIG,
                 self.ELCRIT, self.TLCRIT, self.ENTP, self.SIGD, self.SIGS,
                 self.OMTRAIN, self.OMTSNOW, self.COEFFR, self.COEFFS,
                 self.CU, self.BETA, self.DTMAX, self.ALPHA, self.DAMP
                 )
     # If dry adjustment is being used then the tendencies need to be adjusted
     if self.IPBL != 0:
         FT += (Tout - T) / DELT
         FQ += (Qout - Q) / DELT
     tendencies = {'Tatm': _convect_to_climlab(FT)*np.ones_like(self.state['Tatm']),
                   'q': _convect_to_climlab(FQ)*np.ones_like(self.state['q'])}
     if 'Ts' in self.state:
         # for some strange reason self.Ts is breaking tests under Python 3.5 in some configurations
         tendencies['Ts'] = 0. * self.state['Ts']
     if 'U' in self.state:
         tendencies['U'] = _convect_to_climlab(FU) * np.ones_like(self.state['U'])
     if 'V' in self.state:
         tendencies['V'] = _convect_to_climlab(FV) * np.ones_like(self.state['V'])
     self.CBMF = CBMFnew
     #  Need to convert from mm/day to mm/s or kg/m2/s
     #  Hack to handle single column and multicolumn
     if self.multidim:
         self.precipitation[:,0] = _convect_to_climlab(PRECIP)/const.seconds_per_day
     else:
         self.precipitation[:] = _convect_to_climlab(PRECIP)/const.seconds_per_day
     self.IFLAG = IFLAG
     self.relative_humidity[:] = self.q / qsat(self.Tatm,self.lev)
     return tendencies
Exemple #6
0
 def _compute_flux(self):
     #  specific humidity at lowest model level
     #  assumes pressure is the last axis
     q = Field(self.q[..., -1, np.newaxis], domain=self.Ts.domain)
     Ta = Field(self.Tatm[..., -1, np.newaxis], domain=self.Ts.domain)
     qs = qsat(self.Ts, self.ps)
     Deltaq = Field(qs - q, domain=self.Ts.domain)
     rho = self._air_density(Ta)
     #  flux from bulk formula
     self._flux = const.Lhvap * rho * self.Cd * self.U * Deltaq
     self.LHF = self._flux
Exemple #7
0
 def _compute_flux(self):
     #  specific humidity at lowest model level
     #  assumes pressure is the last axis
     q = self.q[..., -1, np.newaxis]
     Ta = self.Tatm[..., -1, np.newaxis]
     qs = qsat(self.Ts, self.ps)
     Deltaq = qs - q
     rho = self._air_density(Ta)
     #  flux from bulk formula
     self._flux = const.Lhvap * rho * self.Cd * self.U * Deltaq
     self.LHF = self._flux
Exemple #8
0
 def compute_flux(self):
     #  specific humidity at lowest model level
     #  assumes pressure is the last axis
     q = self.q[..., 0, np.newaxis]
     Ta = self.Tatm[..., 0, np.newaxis]
     qs = qsat(self.Ts, const.ps)
     Deltaq = qs - q
     #  air density
     rho = const.ps * const.mb_to_Pa / const.Rd / Ta
     #  flux from bulk formula
     self.flux = const.Lhvap * rho * self.Cd * self.U * Deltaq
     self.diagnostics['LHF'] = self.flux
Exemple #9
0
 def compute_flux(self):
     #  specific humidity at lowest model level
     #  assumes pressure is the last axis
     q = self.q[..., 0, np.newaxis]
     Ta = self.Tatm[..., 0, np.newaxis]
     qs = qsat(self.Ts, const.ps)
     Deltaq = qs - q
     #  air density
     rho = const.ps * const.mb_to_Pa / const.Rd / Ta
     #  flux from bulk formula
     self.flux = const.Lhvap * rho * self.Cd * self.U * Deltaq
     self.diagnostics['LHF'] = self.flux
Exemple #10
0
 def _compute_flux(self):
     #  specific humidity at lowest model level
     #  assumes pressure is the last axis
     q = Field(self.q[..., -1, np.newaxis], domain=self.Ts.domain)
     Ta = Field(self.Tatm[..., -1, np.newaxis], domain=self.Ts.domain)
     qs = qsat(self.Ts, self.ps)
     Deltaq = Field(qs - q, domain=self.Ts.domain)
     rho = self._air_density(Ta)
     #  flux from bulk formula
     self._flux = self.resistance * const.Lhvap * rho * self.Cd * self.U * Deltaq
     self.LHF[:] = self._flux
     # evporation rate, convert from W/m2 to kg/m2/s (or mm/s)
     self.evaporation[:] = self.LHF / const.Lhvap
Exemple #11
0
 def _compute_flux(self):
     #  specific humidity at lowest model level
     #  assumes pressure is the last axis
     q = Field(self.q[..., -1, np.newaxis], domain=self.Ts.domain)
     Ta = Field(self.Tatm[..., -1, np.newaxis], domain=self.Ts.domain)
     qs = qsat(self.Ts, self.ps)
     Deltaq = Field(qs - q, domain=self.Ts.domain)
     rho = self._air_density(Ta)
     #  flux from bulk formula
     self._flux = self.resistance * const.Lhvap * rho * self.Cd * self.U * Deltaq
     self.LHF[:] = self._flux
     # evporation rate, convert from W/m2 to kg/m2/s (or mm/s)
     self.evaporation[:] = self.LHF/const.Lhvap
Exemple #12
0
def test_thermo():
    '''Basic single value tests for the thermodynamic routines.'''
    assert np.isclose(thermo.potential_temperature(250., 500.), 304.783)
    assert np.isclose(thermo.theta(250., 500.), 304.783)

    assert np.isclose(thermo.temperature_from_potential(300., 500.), 246.076)
    assert np.isclose(thermo.T(300., 500.), 246.076)

    assert np.isclose(thermo.clausius_clapeyron(300.), 35.345)

    assert np.isclose(thermo.qsat(300., 1000.), 0.02227839)

    assert np.isclose(thermo.estimated_inversion_strength(300., 290.), 5.3605345)
    assert np.isclose(thermo.EIS(300., 290.), 5.3605345)

    assert np.isclose(thermo.blackbody_emission(300.), 459.3)
Exemple #13
0
def test_thermo():
    '''Basic single value tests for the thermodynamic routines.'''
    assert np.isclose(thermo.potential_temperature(250., 500.), 304.783)
    assert np.isclose(thermo.theta(250., 500.), 304.783)

    assert np.isclose(thermo.temperature_from_potential(300., 500.), 246.076)
    assert np.isclose(thermo.T(300., 500.), 246.076)

    assert np.isclose(thermo.clausius_clapeyron(300.), 35.345)

    assert np.isclose(thermo.qsat(300., 1000.), 0.02227839)

    assert np.isclose(thermo.estimated_inversion_strength(300., 290.), 2.58025)
    assert np.isclose(thermo.EIS(300., 290.), 2.58025)

    assert np.isclose(thermo.blackbody_emission(300.), 459.3)
Exemple #14
0
def toa_flux(ctrl, pert, kernels, cloud_mask_correction=0.):
    '''Compute changes in TOA flux using radiative kernels.
    Inputs are xray dataset handles for model climatology files,
    and another dataset for the kernels.
    
    The sign convention is that the computed flux is POSITIVE DOWN
    (source of energy)
    thus the flux has the same sign as the corresponding feedback
    once normalized by surface temperature change.

    Currently implemented for CAM4 aquaplanet output.'''
    
    #   The kernels are not on the same grid as the CAM4 output
    lev_kernel = kernels.pfull
    dp_kernel = np.tile(np.transpose(np.atleast_2d(np.diff(kernels.phalf.data))),(1,90))
    
    # Flux is calculated by convolving the kernel with 
    #  temperature / humidity response in the TROPOSPHERE
    
    #   define a mask from surface to 100 mb at equator, 
    #    decreasing linearly to 300 mb at the poles 
    #      (following Feldl and Roe 2013)
    maxlev = np.tile(100. + (200. * np.abs(kernels.lat)/90.), (lev_kernel.size, 1))
    p_kernel = np.tile(np.expand_dims(lev_kernel, axis=1), (1, kernels.lat.size))
    dp_masked = np.ma.masked_array(dp_kernel, np.where(p_kernel > maxlev, False, True))
    
    #  this custom function will compute vertical integrals
    #  taking advantage of xray named coordinates
    def integral(field):
        return (field * dp_masked).sum(dim='pfull')

    #  Temperature anomalies at every point in the latitude - pressure plane
    DeltaT = pert.data_vars['T'] - ctrl.data_vars['T']
    #  Surface temperature anomalies
    DeltaTS = pert.data_vars['TS'] - ctrl.data_vars['TS']
    
    #  2D interpolation for the tropospheric temperature anomalies
    #   here I think I need to abandon the xray dataset and just use numpy
    field = regrid(ctrl.lat.data, ctrl.lev.data, DeltaT.data, 
                   kernels.lat.data, lev_kernel.data)
    DeltaT_interp = np.ma.masked_array(field, np.isnan(field))
    #  These use the "masked array" module (part of numpy) 
    #   to make it easy to mask out missing data
    #  1D interpolation for the surface temperature anomalies
    field = np.interp(kernels.lat, DeltaTS.lat, DeltaTS)
    DeltaTS_interp = np.ma.masked_array(field, np.isnan(field))

    #  finite difference approximation to the slope d/dT (q_saturation)
    small = 0.01
    dqsatdT = (qsat(ctrl.data_vars['T']+small, ctrl.lev) - 
               qsat(ctrl.data_vars['T']-small, ctrl.lev)) / (2*small)
    
    #  actual specific humidity anomalies
    DeltaQ = pert.Q - ctrl.Q
    #  relative humidity in control run (convert from percent to fraction)
    RH_ctrl = ctrl.RELHUM / 100.
    
    #  Equivalent temperature change
    #  (actual humidity change expressed as temperature change at fixed RH)
    DeltaTequiv = DeltaQ / (RH_ctrl * dqsatdT )
    #  Scaled by local surface temp. anomaly
    #DeltaTequiv_scaled = DeltaTequiv / DeltaTS
     #  But actually we are supposed to be using log(q)    
    DeltaLogQ = np.log(pert.Q) - np.log(ctrl.Q)
    dlogqsatdT = (np.log(qsat(ctrl.data_vars['T']+small, ctrl.lev)) - 
               np.log(qsat(ctrl.data_vars['T']-small, ctrl.lev))) / (2*small)
    DeltaTequiv_log = DeltaLogQ / (dlogqsatdT)
    #  Interpolated to kernel grid:
    field = regrid(ctrl.lat.data, ctrl.lev.data, DeltaTequiv_log.data, 
                   kernels.lat.data, lev_kernel.data)
    DeltaTequiv_interp = np.ma.masked_array(field, np.isnan(field))

    #  create a new dictionary to hold all the feedbacks
    flux = {}
    #   Compute the feedbacks!
    flux['lw_q'] = integral(kernels['lw_q'] * DeltaTequiv_interp)
    #  shortwave water vapor
    flux['sw_q'] = integral(kernels['sw_q'] * DeltaTequiv_interp)
    #  longwave temperature  (Planck and lapse rate)
    flux['Planck'] =  (kernels['lw_ts'] +
                       integral(kernels['lw_t'])) * DeltaTS_interp
    flux['lapse'] = integral(kernels['lw_t'] * (DeltaT_interp-DeltaTS_interp)) 
    flux['lw_t'] = flux['Planck'] + flux['lapse']
    #  Add up the longwave feedbacks
    flux['lw_net'] = flux['lw_t'] + flux['lw_q']

    #  Compute cloud radiative forcing
        #   first, the all-sky TOA radiation fields
    #try:
    #    DeltaOLR = pert.FLNT - ctrl.FLNT
    #    DeltaASR = pert.FSNT - ctrl.FSNT
    #        #  and the clear-sky diagnostics
    #    DeltaOLRclr = pert.FLNTC - ctrl.FLNTC
    #    DeltaASRclr = pert.FSNTC - ctrl.FSNTC
    #    #  Cloud radiative forcing is just the difference between these:
    #except:
    DeltaOLR = pert.FLNT.data - ctrl.FLNT.data 
    DeltaASR = pert.FSNT.data  - ctrl.FSNT.data 
        #  and the clear-sky diagnostics
    DeltaOLRclr = pert.FLNTC.data  - ctrl.FLNTC.data 
    DeltaASRclr = pert.FSNTC.data  - ctrl.FSNTC.data 
        #  Cloud radiative forcing is just the difference between these:
    DeltaOLRcld = DeltaOLR  - DeltaOLRclr 
    DeltaASRcld = DeltaASR  - DeltaASRclr

    #  Soden et al. 2008 show how to compute the cloud feedback accurately 
    #  accounting for cloud masking --  see their equation (25)

    # The cloud radiative forcing itself, interpolated
    flux['CRF_sw'] = np.interp( kernels.lat, ctrl.lat, DeltaASRcld )
    flux['CRF_lw'] = np.interp( kernels.lat, ctrl.lat, -DeltaOLRcld )
    flux['CRF_net'] = flux['CRF_sw'] + flux['CRF_lw']
    #  The corrections...
    #    water vapor
    flux['cld_q_lw'] = integral(kernels['cld_q_lw'] * DeltaTequiv_interp)
    flux['cld_q_sw'] = integral(kernels['cld_q_sw'] * DeltaTequiv_interp)
    flux['cld_q'] = flux['cld_q_lw'] + flux['cld_q_sw']
    #    temperature
    flux['cld_t'] = ( kernels['cld_ts']*DeltaTS_interp + 
                    integral(kernels['cld_t'] * DeltaT_interp) )   
    #  Corrected cloud feedbacks
    flux['cloud_lw'] = flux['CRF_lw'] + flux['cld_t'] + flux['cld_q_lw']
    flux['cloud_sw'] = flux['CRF_sw'] + flux['cld_q_sw']
    flux['cloud'] = flux['cloud_lw'] + flux['cloud_sw']
    #    Soden et al. (2008) argue that there should be an additional 
    #     correction due to cloud masking of radiative forcing...
    #    This is only an issue for 2xCO2 forcing.  
    #    Soden et al. suggest 0.69 W/m2/K globally 
    flux['cloud'] += cloud_mask_correction
    
    #  Total net feedback
    flux['total'] = flux['cloud'] + flux['lw_net'] + flux['sw_q']

    # Alternate description using fixed relative humidity as state
    # variable following Held and Shell J. Clim. 2012
    
    # temperature kernel for fixed relative humidity is the sum of
    #  traditional temperature and traditional water vapor kernels
    kernels['lw_talt'] = kernels['lw_t'] + kernels['lw_q']
    flux['Planck_alt'] =  (kernels['lw_ts'] +
                           integral(kernels['lw_talt'])) * DeltaTS_interp
    flux['lapse_alt'] = integral(kernels['lw_talt'] * 
                                 (DeltaT_interp-DeltaTS_interp))
    # Get RH feedback by subtracting original water vapor kernel times 
    # atmospheric temperature response from traditional water vapor feedback.
    flux['RH'] = flux['lw_q'] - integral(kernels['lw_q'] * DeltaT_interp)
    
    #  package output into xray datasets
    flux_dataset = xray.Dataset(variables=flux)
    return flux_dataset
Exemple #15
0
def toa_flux(ctrl, pert, kernels, cloud_mask_correction=0.):
    '''Compute changes in TOA flux using radiative kernels.
    Inputs are xarray dataset handles for model climatology files,
    and another dataset for the kernels.

    The sign convention is that the computed flux is POSITIVE DOWN
    (source of energy)
    thus the flux has the same sign as the corresponding feedback
    once normalized by surface temperature change.

    Currently implemented for CAM4 aquaplanet output.'''
    #   The kernels are not on the same grid as the CAM4 output
    lev_kernel = kernels.pfull
    dp_kernel = np.tile(
        np.transpose(np.atleast_2d(np.diff(kernels.phalf.data))), (1, 90))

    # Flux is calculated by convolving the kernel with
    #  temperature / humidity response in the TROPOSPHERE

    #   define a mask from surface to 100 mb at equator,
    #    decreasing linearly to 300 mb at the poles
    #      (following Feldl and Roe 2013)
    maxlev = np.tile(100. + (200. * np.abs(kernels.lat) / 90.),
                     (lev_kernel.size, 1))
    p_kernel = np.tile(np.expand_dims(lev_kernel, axis=1),
                       (1, kernels.lat.size))
    dp_masked = np.ma.masked_array(dp_kernel,
                                   np.where(p_kernel > maxlev, False, True))

    #  this custom function will compute vertical integrals
    #  taking advantage of xarray named coordinates
    def integral(field):
        return (field * dp_masked).sum(dim='pfull')

    #  Temperature anomalies at every point in the latitude - pressure plane
    DeltaT = pert.data_vars['TA'] - ctrl.data_vars['TA']
    #  Surface temperature anomalies
    DeltaTS = pert.data_vars['TS'] - ctrl.data_vars['TS']

    #  2D interpolation for the tropospheric temperature anomalies
    #   here I think I need to abandon the xarray dataset and just use numpy
    field = regrid(ctrl.lat.data, ctrl.lev.data, DeltaT.data, kernels.lat.data,
                   lev_kernel.data)
    DeltaT_interp = np.ma.masked_array(field, np.isnan(field))
    #  These use the "masked array" module (part of numpy)
    #   to make it easy to mask out missing data
    #  1D interpolation for the surface temperature anomalies
    field = np.interp(kernels.lat, DeltaTS.lat, DeltaTS)
    DeltaTS_interp = np.ma.masked_array(field, np.isnan(field))

    #  finite difference approximation to the slope d/dT (q_saturation)
    small = 0.01
    dqsatdT = (qsat(ctrl.data_vars['TA'] + small, ctrl.lev) -
               qsat(ctrl.data_vars['TA'] - small, ctrl.lev)) / (2 * small)

    #  actual specific humidity anomalies
    DeltaQ = pert.Q - ctrl.Q
    #  relative humidity in control run (convert from percent to fraction)
    RH_ctrl = ctrl.RELHUM / 100.

    #  Equivalent temperature change
    #  (actual humidity change expressed as temperature change at fixed RH)
    DeltaTequiv = DeltaQ / (RH_ctrl * dqsatdT)
    #  Scaled by local surface temp. anomaly
    #DeltaTequiv_scaled = DeltaTequiv / DeltaTS
    #  But actually we are supposed to be using log(q)
    DeltaLogQ = np.log(pert.Q) - np.log(ctrl.Q)
    dlogqsatdT = (np.log(qsat(ctrl.data_vars['TA'] + small, ctrl.lev)) -
                  np.log(qsat(ctrl.data_vars['TA'] - small, ctrl.lev))) / (
                      2 * small)
    DeltaTequiv_log = DeltaLogQ / (dlogqsatdT)
    #  Interpolated to kernel grid:
    field = regrid(ctrl.lat.data, ctrl.lev.data, DeltaTequiv_log.data,
                   kernels.lat.data, lev_kernel.data)
    DeltaTequiv_interp = np.ma.masked_array(field, np.isnan(field))

    #  create a new dictionary to hold all the feedbacks
    flux = {}
    #   Compute the feedbacks!
    flux['lw_q'] = integral(kernels['lw_q'] * DeltaTequiv_interp)
    #  shortwave water vapor
    flux['sw_q'] = integral(kernels['sw_q'] * DeltaTequiv_interp)
    #  longwave temperature  (Planck and lapse rate)
    flux['Planck'] = (kernels['lw_ts'] +
                      integral(kernels['lw_t'])) * DeltaTS_interp
    flux['lapse'] = integral(kernels['lw_t'] *
                             (DeltaT_interp - DeltaTS_interp))
    flux['lw_t'] = flux['Planck'] + flux['lapse']
    #  Add up the longwave feedbacks
    flux['lw_net'] = flux['lw_t'] + flux['lw_q']

    #  Compute cloud radiative forcing
    #   first, the all-sky TOA radiation fields
    DeltaOLR = pert.FLNT - ctrl.FLNT
    DeltaASR = pert.FSNT - ctrl.FSNT
    #  and the clear-sky diagnostics
    DeltaOLRclr = pert.FLNTC - ctrl.FLNTC
    DeltaASRclr = pert.FSNTC - ctrl.FSNTC
    #  Cloud radiative forcing is just the difference between these:
    DeltaOLRcld = DeltaOLR - DeltaOLRclr
    DeltaASRcld = DeltaASR - DeltaASRclr

    #  Soden et al. 2008 show how to compute the cloud feedback accurately
    #  accounting for cloud masking --  see their equation (25)

    # The cloud radiative forcing itself, interpolated
    flux['CRF_sw'] = np.interp(kernels.lat, ctrl.lat, DeltaASRcld)
    flux['CRF_lw'] = np.interp(kernels.lat, ctrl.lat, -DeltaOLRcld)
    flux['CRF_net'] = flux['CRF_sw'] + flux['CRF_lw']
    #  The corrections...
    #    water vapor
    flux['cld_q_lw'] = integral(kernels['cld_q_lw'] * DeltaTequiv_interp)
    flux['cld_q_sw'] = integral(kernels['cld_q_sw'] * DeltaTequiv_interp)
    flux['cld_q'] = flux['cld_q_lw'] + flux['cld_q_sw']
    #    temperature
    flux['cld_t'] = (kernels['cld_ts'] * DeltaTS_interp +
                     integral(kernels['cld_t'] * DeltaT_interp))
    #  Corrected cloud feedbacks
    flux['cloud_lw'] = flux['CRF_lw'] + flux['cld_t'] + flux['cld_q_lw']
    flux['cloud_sw'] = flux['CRF_sw'] + flux['cld_q_sw']
    flux['cloud'] = flux['cloud_lw'] + flux['cloud_sw']
    #    Soden et al. (2008) argue that there should be an additional
    #     correction due to cloud masking of radiative forcing...
    #    This is only an issue for 2xCO2 forcing.
    #    Soden et al. suggest 0.69 W/m2/K globally
    flux['cloud'] += cloud_mask_correction

    #  Total net feedback
    flux['total'] = flux['cloud'] + flux['lw_net'] + flux['sw_q']

    # Alternate description using fixed relative humidity as state
    # variable following Held and Shell J. Clim. 2012

    # temperature kernel for fixed relative humidity is the sum of
    #  traditional temperature and traditional water vapor kernels
    kernels['lw_talt'] = kernels['lw_t'] + kernels['lw_q']
    flux['Planck_alt'] = (kernels['lw_ts'] +
                          integral(kernels['lw_talt'])) * DeltaTS_interp
    flux['lapse_alt'] = integral(kernels['lw_talt'] *
                                 (DeltaT_interp - DeltaTS_interp))
    # Get RH feedback by subtracting original water vapor kernel times
    # atmospheric temperature response from traditional water vapor feedback.
    flux['RH'] = flux['lw_q'] - integral(kernels['lw_q'] * DeltaT_interp)

    #  package output into xarray datasets
    flux_dataset = xr.Dataset(data_vars=flux)
    return flux_dataset