Binning spectra#

Module crt1d.spectra provides some tools for working with spectra. By binning or smearing spectra, we can use spectra together that were originally on different grids. Also, we can decrease canopy RT computational time by reducing spectral resolution.

import matplotlib.pyplot as plt
import numpy as np
from scipy import stats

import crt1d as crt

Solar#

Although the main purpose of the “smear” functions is to provide the ability to go to lower spectral resolution while conserving certain properties, they also work as interpolators when a higher resolution grid is used.

Note

The bins do not have to be equal-width—variable grid will also work!

ds0 = crt.data.load_default_sp2()
ds0
<xarray.Dataset> Size: 8kB
Dimensions:  (wl0: 122, wl: 121, wle: 122)
Coordinates:
  * wl       (wl) float64 968B 0.3025 0.3075 0.3125 0.3175 ... 3.75 3.85 3.95
  * wl0      (wl0) float64 976B 0.3 0.305 0.31 0.315 0.32 ... 3.7 3.8 3.9 4.0
  * wle      (wle) float64 976B 0.3 0.305 0.31 0.315 0.32 ... 3.7 3.8 3.9 4.0
Data variables:
    SI_dr    (wl0) float64 976B 0.26 4.388 16.17 40.28 ... 9.046 8.127 8.219
    SI_df    (wl0) float64 976B 0.7224 10.65 34.92 ... 0.2995 0.2616 0.2653
    I_dr     (wl) float64 968B 0.01162 0.05141 0.1411 ... 0.9325 0.8587 0.8173
    I_df     (wl) float64 968B 0.02844 0.1139 0.2843 ... 0.03113 0.02806 0.02635
    dwl      (wl) float64 968B 0.005 0.005 0.005 0.005 0.005 ... 0.1 0.1 0.1 0.1
binss = [
    [0.3, 0.4, 0.5, 0.6, 0.7, 1.0, 1.5, 2.5],  # variable dx is supported
    np.linspace(0.3, 2.5, 8),
    np.linspace(0.3, 2.5, 21),
    np.linspace(0.3, 2.5, 201),
]

fig, ax = plt.subplots(2, 2, figsize=(11, 7))

for bins, ax in zip(binss, ax.flat):
    # The spectral variables have coord `wl0`
    crt.spectra.plot_binned_ds(ds0, crt.spectra.smear(ds0, bins, x="wl0"), yname="SI_df", xtight="bins", ax=ax)
../_images/70cc8cd300f00bc5ddae11ff92a4c29ebda4d04c7fc702e3f29a540ed080bec4.png

Warning

Irradiance (W m-2) should not be smeared directly (as we see below). Instead we use a special routine to calculate in-bin irradiance by smearing spectral irradiance (W m-2 μm-1).

If we smear irradiance directly, the total irradiance is essentially proportional to the number of bins used. For equal-width bins, it turns out to be exactly proportional.

tot0 = ds0.I_dr.sum().values
tot = []
nbands = np.arange(1, 200, 10)
for n in nbands:
    tot.append(crt.spectra.smear(ds0, np.linspace(0.3, 4.0, n)).I_dr.sum().values)

res = stats.linregress(nbands, tot)

fig, ax = plt.subplots()
ax.plot(nbands, tot, ".-")
ax.text(0.03, 0.96, f"Original total: {tot0:.4g}\nr$^2 = {res.rvalue**2:.3f}$", ha="left", va="top", transform=ax.transAxes)
ax.set(xlabel="# of bands", ylabel="Total irradiance [W m-2]");
../_images/16920105e2410239c0780ba5330cabd3b0fa06db649606d762e70f2ddae9b162.png

Instead, for computing irradiance in bins, crt1d.spectra.smear_si() can be used for convenience. It smears spectral irradiance and then multiplies by bin width to give irradiance.

Leaf/soil optical properties#

ds0_l = crt.data.load_ideal_leaf().dropna(dim="wl")
ds0_l
<xarray.Dataset> Size: 3kB
Dimensions:  (wl: 108)
Coordinates:
  * wl       (wl) float64 864B 0.3 0.305 0.31 0.315 0.32 ... 2.36 2.45 2.5 2.6
Data variables:
    rl       (wl) float64 864B 0.1253 0.1241 0.1229 ... 0.06321 0.03968 1e-10
    tl       (wl) float64 864B 0.08399 0.08376 0.08352 ... 0.0699 0.03549 1e-10
bins2 = np.linspace(0.3, 2.6, 21)
ds1_l = crt.spectra.smear(ds0_l, bins2)
crt.spectra.plot_binned_ds(ds0_l, ds1_l, yname="rl")
../_images/7f0711b81f833072f955b11d0baff79322e545b5880b78eeb2b85f87cd6a1eca.png

Function crt1d.spectra.smear() can be applied to array-like, xarray.DataArray, or xarray.Dataset.

# `x` array must be provided if passing array
crt.spectra.smear(ds1_l.rl.values, bins2, x=ds1_l.wl)
array([0.05487465, 0.11476666, 0.16763666, 0.23817439, 0.48307759,
       0.53507554, 0.52547132, 0.49396821, 0.42819319, 0.34355597,
       0.29599888, 0.34741731, 0.38775264, 0.34816994, 0.22202369,
       0.15598805, 0.19195755, 0.15037308, 0.08022294, 0.01829059])
# `x` assumed to be the variable with `name` `'wl'`
crt.spectra.smear(ds1_l.rl, bins2)
<xarray.Dataset> Size: 488B
Dimensions:  (wl: 20, wle: 21)
Coordinates:
  * wl       (wl) float64 160B 0.3575 0.4725 0.5875 0.7025 ... 2.312 2.428 2.543
  * wle      (wle) float64 168B 0.3 0.415 0.53 0.645 ... 2.255 2.37 2.485 2.6
Data variables:
    rl       (wl) float64 160B 0.05487 0.1148 0.1676 ... 0.1504 0.08022 0.01829
crt.spectra.smear(ds1_l, bins2)
<xarray.Dataset> Size: 648B
Dimensions:  (wl: 20, wle: 21)
Coordinates:
  * wl       (wl) float64 160B 0.3575 0.4725 0.5875 0.7025 ... 2.312 2.428 2.543
  * wle      (wle) float64 168B 0.3 0.415 0.53 0.645 ... 2.255 2.37 2.485 2.6
Data variables:
    rl       (wl) float64 160B 0.05487 0.1148 0.1676 ... 0.1504 0.08022 0.01829
    tl       (wl) float64 160B 0.04105 0.09165 0.1316 ... 0.1682 0.09196 0.01898

Warning

Smearing leaf/soil optical properties directly causes issues as well, though more subtle than for irradiance.

Reflectance/transmittance are like albedo in that they depend on the incoming light as well. We can account for this by weighting with a light spectrum when averaging the optical property spectra.

The influence of accounting for the incoming light spectrum is most apparent when the bins are large and few.

bins3 = np.linspace(0.3, 2.6, 4)

fig, axs = plt.subplots(2, 1, figsize=(6, 6))

for method, ax in zip(["tuv", "avg_optical_prop"], axs.flat):
    crt.spectra.plot_binned_ds(ds0_l, crt.spectra.smear(ds0_l, bins3, method=method), yname="rl", ax=ax)
../_images/63dd4d0a4513ff34b32794913cdcf5bdfd5d28c23ec9316672273f56b418ac4e.png

Average optical properties#

Below, we can see that when we compute the average reflectance, over certain regions the above doesn’t matter much (PAR/visible). In the NIR, however, there is a strong preference for smaller wavelengths, which overall weights leaf reflectance towards higher values.

In the below, 'uniform' corresponds to standard smearing, with no solar weighting.

First, we calculate average values using the original spectrum, loaded above.

# On the original leaf reflectance spectrum
boundss = [(0.4, 0.7), (0.7, 1.0), (1.0, 2.5), (0.3, 2.5)]
lights = ["planck", "uniform"]

for bounds in boundss:
    print(bounds)
    for light in lights:
        ybar = crt.spectra.avg_optical_prop(
            ds0_l.rl, bounds, x=ds0_l.wl, light=light
        )
        print(f" {light}: {ybar:.4g}")
(0.4, 0.7)
 planck: 0.1316
 uniform: 0.1317
(0.7, 1.0)
 planck: 0.4641
 uniform: 0.4812
(1.0, 2.5)
 planck: 0.3956
 uniform: 0.3017
(0.3, 2.5)
 planck: 0.2744
 uniform: 0.2944

Now, we use the spectrum smeared above.

# On a smeared/binned leaf reflectance spectrum
ds2 = crt.spectra.smear(ds0, bins2)
data = (
    ds2.sel(wl=ds1_l.wl.values, method="nearest").I_dr +
    ds2.sel(wl=ds1_l.wl.values, method="nearest").I_df
)
lights = ["planck", "uniform", data]

for bounds in boundss:
    print(bounds)
    for light in lights:
        ybar = crt.spectra.avg_optical_prop(
            ds1_l.rl, bounds, xe=ds1_l.wle, light=light
        )
        if isinstance(light, str):
            print(f" {light}: {ybar:.4g}")
        else:
            print(f" data: {ybar:.4g}")
(0.4, 0.7)
 planck: 0.1463
 uniform: 0.1492
 data: 0.1581
(0.7, 1.0)
 planck: 0.4401
 uniform: 0.4641
 data: 0.4382
(1.0, 2.5)
 planck: 0.3953
 uniform: 0.3016
 data: 0.3849
(0.3, 2.5)
 planck: 0.2754
 uniform: 0.2943
 data: 0.3197

👆 Notice that the values for 'uniform' and 'planck' are similar (though not identical) to the calculations on the pre-smeared spectrum.

Planck function#

Planck radiance is one of the light options can be used to weight values when smearing the spectra with the method='avg_optical_prop' option. This is important for albedo, for example, because albedo in some waveband (such as visible or shortwave) depends on both the albedo spectrum and the spectrum of the illuminating light.

wl_um = np.linspace(0.3, 2.6, 200)
fig, ax = plt.subplots()
for T_K in [4000, 5000, 5800, 6000]:
    ax.plot(wl_um, crt.spectra.l_wl_planck(T_K, wl_um), label=T_K)
ax.set(
    xlabel=r"Wavelength $\lambda$ [μm]",
    ylabel=r"Spectral radiance $L(\lambda)$ [W sr$^{-1}$ m$^{-2}$ m$^{-1}$]",
)
ax.legend(title="Blackbody temperature (K)");
../_images/6988f8e3f9f02406b38462460eb7872a1bec1ef2885c0209eef1c3a283838ec3.png