# Source code for climlab.radiation.nband

```
from __future__ import division
from builtins import range
import numpy as np
from climlab.radiation.greygas import GreyGas
from climlab import constants as const
from climlab.domain import domain, axis, field
from copy import copy
[docs]
class NbandRadiation(GreyGas):
'''Process for radiative transfer.
Solves the discretized Schwarschild two-stream equations
with the spectrum divided into N spectral bands.
Every NbandRadiation object has an attribute
``self.band_fraction``
with sum(self.band_fraction) == 1
that gives the fraction of the total beam in each band
Also a dictionary
``self.absorber_vmr``
that gives the volumetric mixing ratio of every absorbing gas
on the same grid as temperature
and a dictionary
``self.absorption_cross_section``
that gives the absorption cross-section per unit mass for each gas
in every spectral band
'''
def __init__(self, absorber_vmr=None, **kwargs):
super(NbandRadiation, self).__init__(**kwargs)
newinput = ['band_fraction',
'absorber_vmr',
'absorption_cross_section',
'cosZen',]
self.declare_input(newinput)
# this should be overridden by daughter classes
self.band_fraction = np.array(1.)
## a dictionary of absorbing gases, in volumetric mixing ratios
# each item should have dimensions of self.Tatm
# Can be passed as input argument
if absorber_vmr is None:
absorber_vmr = {}
self.absorber_vmr = absorber_vmr
# a dictionary of absorption cross-sections in m**2 / kg
# each item should have dimension... (num_channels, 1)
self.absorption_cross_section = {}
self.cosZen = 1. # cosine of the average zenith angle
dp = self.Tatm.domain.lev.delta
self.mass_per_layer = dp * const.mb_to_Pa / const.g
self.albedo_sfc = np.ones_like(self.band_fraction) * self.albedo_sfc
@property
def band_fraction(self):
return self._band_fraction
@band_fraction.setter
def band_fraction(self, value):
self.num_channels = value.size
# abstract axis for channels
ax = axis.Axis(num_points=self.num_channels)
self.channel_ax = {'channel': ax}
dom = domain._Domain(axes=self.channel_ax)
# fraction of the total solar flux in each band:
self._band_fraction = field.Field(value, domain=dom)
def _compute_optical_path(self):
# this will cause a problem for a model without CO2
tau = np.zeros_like(self.absorber_vmr['CO2']*
self.absorption_cross_section['CO2'])
for gas, vmr in self.absorber_vmr.items():
# convert to mass of absorber per unit total mass
if gas == 'H2O': # H2O is stored as specific humidity, not VMR
q = vmr
else:
q = vmr / (1.+vmr)
try:
# if this gas isn't present in absorption dictionary
# the assumption is that there is no absorption!
kappa = self.absorption_cross_section[gas]
tau += q * kappa
except: pass
tau *= self.mass_per_layer / self.cosZen
return tau
def _compute_absorptivity(self):
# assume that the water vapor etc is current
optical_path = self._compute_optical_path()
# account for finite layer depth
absorptivity = 1. - np.exp(-optical_path)
axes = copy(self.Tatm.domain.axes)
# add these to the dictionary of axes
axes.update(self.channel_ax)
dom = domain.Atmosphere(axes=axes)
self.absorptivity = field.Field(absorptivity, domain=dom)
def _compute_emission_sfc(self):
# need to split the total emission across the bands
total_emission = super(NbandRadiation, self)._compute_emission_sfc()
return self._split_channels(total_emission)
def _compute_emission(self):
# need to split the total emission across the bands
total_emission = super(NbandRadiation, self)._compute_emission()
band_fraction = self.band_fraction
for n in range(self.Tatm.domain.numdims):
band_fraction = band_fraction[:, np.newaxis]
return total_emission * band_fraction
def _compute_radiative_heating(self):
# need to recompute transmissivities each time because
# water vapor is changing
self._compute_absorptivity()
super(NbandRadiation, self)._compute_radiative_heating()
def _split_channels(self, flux):
split = np.outer(self.band_fraction, flux)
# make sure there's a singleton dimension at the last axis (level)
if np.size(split, axis=-1) != 1:
split = split[..., np.newaxis]
return split
def _join_channels(self, flux):
return np.sum(flux, axis=0)
[docs]
class ThreeBandSW(NbandRadiation):
def __init__(self, emissivity_sfc=0., **kwargs):
'''A three-band mdoel for shortwave radiation.
The spectral decomposition used here is largely based on the
"Moist Radiative-Convective Model" by Aarnout van Delden, Utrecht University
a.j.vandelden@uu.nl
http://www.staff.science.uu.nl/~delde102/RCM.htm
Three SW channels:
channel 0 is Hartley and Huggins band (UV, 1%, 200 - 340 nm)
channel 1 is Chappuis band (27%, 450 - 800 nm)
channel 2 is remaining radiation (72%)
'''
super(ThreeBandSW, self).__init__(emissivity_sfc=emissivity_sfc, **kwargs)
# fraction of the total solar flux in each band:
self.band_fraction = np.array([0.01, 0.27, 0.72])
if 'CO2' not in self.absorber_vmr:
self.absorber_vmr['CO2'] = 380.E-6 * np.ones_like(self.Tatm)
if 'O3' not in self.absorber_vmr:
self.absorber_vmr['O3'] = np.zeros_like(self.Tatm)
if 'H2O' not in self.absorber_vmr:
self.absorber_vmr['H2O'] = self.q
## absorption cross-sections in m**2 / kg
O3 = np.array([200.E-24, 0.285E-24, 0.]) * const.Rd / const.kBoltzmann
#self.absorption_cross_section['O3'] = np.reshape(O3,
# (self.num_channels, 1))
#H2O = np.array([0.002, 0.002, 0.002])
H2O = np.array([0., 0., 0.001])
for n in range(self.Tatm.domain.numdims):
H2O = H2O[:, np.newaxis]
O3 = O3[:, np.newaxis]
self.absorption_cross_section['O3'] = O3
self.absorption_cross_section['H2O'] = H2O
#self.absorption_cross_section['H2O'] = np.reshape(H2O,
# (self.num_channels, 1))
self.absorption_cross_section['CO2'] = \
np.zeros_like(self.absorption_cross_section['O3'])
self.cosZen = 0.5 # cosine of the average solar zenith angle
@property
def emissivity(self):
# This ensures that emissivity is always zero for shortwave classes
return np.zeros_like(self.absorptivity)
[docs]
class FourBandSW(NbandRadiation):
# The most recent RMCM program uses a four-channel SW model
# this is probably a better way to do it... distinguishes between
# visible band with no absorption and near-infrared with weak H2O absorption
# But this needs some tuning and better documentation
def __init__(self, emissivity_sfc=0., **kwargs):
'''A four-band mdoel for shortwave radiation.
The spectral decomposition used here is largely based on the
"Moist Radiative-Convective Model" by Aarnout van Delden, Utrecht University
a.j.vandelden@uu.nl
http://www.staff.science.uu.nl/~delde102/RCM.htm
Four SW channels:
channel 0 is Hartley and Huggins band (UV, 6%, <340 nm)
channel 1 is part of visible with no O3 absorption (14%, 340 - 500 nm)
channel 2 is Chappuis band (27%, 500 - 700 nm)
channel 3 is near-infrared (53%, > 700 nm)
'''
super(FourBandSW, self).__init__(emissivity_sfc=emissivity_sfc, **kwargs)
# fraction of the total solar flux in each band:
self.band_fraction = np.array([0.06, 0.14, 0.27, 0.53])
if 'CO2' not in self.absorber_vmr:
self.absorber_vmr['CO2'] = 380.E-6 * np.ones_like(self.Tatm)
if 'O3' not in self.absorber_vmr:
self.absorber_vmr['O3'] = np.zeros_like(self.Tatm)
if 'H2O' not in self.absorber_vmr:
self.absorber_vmr['H2O'] = self.q
## absorption cross-sections in m**2 / kg
O3 = np.array([200.E-24, 0., 0.285E-24, 0.]) * const.Rd / const.kBoltzmann
#self.absorption_cross_section['O3'] = np.reshape(O3,
# (self.num_channels, 1))
H2O = np.array([0., 0., 0., 0.0012])
for n in range(self.Tatm.domain.numdims):
H2O = H2O[:, np.newaxis]
O3 = O3[:, np.newaxis]
self.absorption_cross_section['O3'] = O3
self.absorption_cross_section['H2O'] = H2O
#self.absorption_cross_section['H2O'] = np.reshape(H2O,
# (self.num_channels, 1))
self.absorption_cross_section['CO2'] = \
np.zeros_like(self.absorption_cross_section['O3'])
self.cosZen = 0.5 # cosine of the average solar zenith angle
@property
def emissivity(self):
# This ensures that emissivity is always zero for shortwave classes
return np.zeros_like(self.absorptivity)
[docs]
class FourBandLW(NbandRadiation):
def __init__(self, **kwargs):
'''Closely following SPEEDY / MITgcm longwave model
band 0 is window region (between 8.5 and 11 microns)
band 1 is CO2 channel (the band of strong absorption by CO2 around 15 microns)
band 2 is weak H2O channel (aggregation of spectral regions with weak to moderate absorption by H2O)
band 3 is strong H2O channel (aggregation of regions with strong absorption by H2O)
'''
super(FourBandLW, self).__init__(**kwargs)
# SPEEDY uses an approximation to the Planck function
# and the band fraction for every emission is calculated from
# its current temperature
# Here for simoplicity we'll just set an average band_fraction
# and hold it fixed
Tarray = np.linspace(-30, 30) + 273.15
self.band_fraction = np.mean(SPEEDY_band_fraction(Tarray), axis=1)
# defaults from MITgcm/aim:
# these are layer absorptivities per dp = 10^5 Pa
# the water vapor terms are expressed for dq = 1 g/kg
ABLWIN = 0.7
ABLCO2 = 4.0
ABLWV1 = 0.7
ABLWV2 = 50.0
# I'm going to assume that the absorption in window region is by O3.
# not sure if this makes any sense... maybe it should be zero
O3 = np.array([ABLWIN, 0., 0., 0.]) / 1E5 * const.g / 5E-6
self.absorption_cross_section['O3'] = np.reshape(O3,
(self.num_channels, 1))
# the CO2 mixing ratio for which SPEEDY / MITgcm is tuned...
# not clear what this number should be
AIMCO2 = 380E-6
CO2 = np.array([0., ABLCO2, 0., 0.]) / 1E5 * const.g / AIMCO2
#self.absorption_cross_section['CO2'] = np.reshape(CO2,
# (self.num_channels, 1))
# Need to multiply by 1E3 for H2O fields because we use kg/kg for mixing ratio
H2O = np.array([0., 0., ABLWV1, ABLWV2]) / 1E5 * const.g * 1E3
#self.absorption_cross_section['H2O'] = np.reshape(H2O,
# (self.num_channels, 1))
for n in range(self.Tatm.domain.numdims):
CO2 = CO2[:, np.newaxis]
O3 = O3[:, np.newaxis]
H2O = H2O[:, np.newaxis]
self.absorption_cross_section.update({'CO2': CO2, 'H2O': H2O, 'O3': O3})
if 'CO2' not in self.absorber_vmr:
self.absorber_vmr['CO2'] = 380.E-6 * np.ones_like(self.Tatm)
if 'O3' not in self.absorber_vmr:
self.absorber_vmr['O3'] = np.zeros_like(self.Tatm)
if 'H2O' not in self.absorber_vmr:
self.absorber_vmr['H2O'] = self.q
[docs]
def SPEEDY_band_fraction(T):
'''Python / numpy implementation of the formula used by SPEEDY and MITgcm
to partition longwave emissions into 4 spectral bands.
Input: temperature in Kelvin
returns: a four-element array of band fraction
Reproducing here the FORTRAN code from MITgcm/pkg/aim_v23/phy_radiat.F
.. code-block:: fortran
EPS3=0.95 _d 0
DO JTEMP=200,320
FBAND(JTEMP,0)= EPSLW
FBAND(JTEMP,2)= 0.148 _d 0 - 3.0 _d -6 *(JTEMP-247)**2
FBAND(JTEMP,3)=(0.375 _d 0 - 5.5 _d -6 *(JTEMP-282)**2)*EPS3
FBAND(JTEMP,4)= 0.314 _d 0 + 1.0 _d -5 *(JTEMP-315)**2
FBAND(JTEMP,1)= 1. _d 0 -(FBAND(JTEMP,0)+FBAND(JTEMP,2)
& +FBAND(JTEMP,3)+FBAND(JTEMP,4))
ENDDO
DO JB=0,NBAND
DO JTEMP=lwTemp1,199
FBAND(JTEMP,JB)=FBAND(200,JB)
ENDDO
DO JTEMP=321,lwTemp2
FBAND(JTEMP,JB)=FBAND(320,JB)
ENDDO
ENDDO
'''
# EPSLW is the fraction of longwave emission that goes directly to space
# It is set to zero by default in MITgcm code. We won't use it here.
Tarray = np.array(T)
Tarray = np.minimum(Tarray, 320.)
Tarray = np.maximum(Tarray, 200.)
num_band = 4
dims = [num_band]
dims.extend(Tarray.shape)
FBAND = np.zeros(dims)
EPS2=0.95
FBAND[1,:] = 0.148 - 3.0E-6 *(T-247.)**2
FBAND[2,:] = (0.375 - 5.5E-6 *(T-282.)**2)*EPS2
FBAND[3,:] = 0.314 + 1.0E-5 *(T-315.)**2
FBAND[0,:] = 1. - np.sum(FBAND, axis=0)
return FBAND
```