157 lines
5.9 KiB
Python
157 lines
5.9 KiB
Python
import numpy as np
|
|
import xarray as xr
|
|
|
|
from brdf_utils import ADF_OCP, solve_2nd_order_poly, drop_unused_coords
|
|
''' Morel et al. (2002) BRDF 校正
|
|
未包含 R gothic
|
|
f/Q 查找表与 NASA SeaDAS 中的一致(详见 BRDF_M02SeaDAS.nc 属性)
|
|
使用 OC4ME 叶绿素反演,进行一次迭代,未应用收敛标准
|
|
'''
|
|
|
|
""" M02 系数的类 """
|
|
|
|
|
|
class Coeffs():
|
|
def __init__(self, foq):
|
|
self.foq = foq
|
|
|
|
|
|
""" Class for M02 BRDF model """
|
|
class M02SeaDAS:
|
|
""" Initialise M02 model: BRDF LUT, coeffs, OC4ME parameters, water IOPs LUT
|
|
Note: bands are fixed and defined at class initilization, but could be initialized in init_pixels if needed
|
|
"""
|
|
|
|
def __init__(self, bands, adf=None):
|
|
if adf is None:
|
|
adf = ADF_OCP
|
|
|
|
# Check required bands are existing, within a 25 nm threshold
|
|
self.bands = bands
|
|
threshold = 25.
|
|
bands_required = [442.5, 490, 510, 560]
|
|
bands_ref = bands.sel(bands=bands_required, method='nearest')
|
|
for band_ref, band_required in zip(bands_ref, bands_required):
|
|
assert abs(band_ref - band_required) < threshold, 'Band %d nm missing or too far' % band_ref
|
|
self.b442 = float(bands_ref[0].item())
|
|
self.b490 = float(bands_ref[1].item())
|
|
self.b510 = float(bands_ref[2].item())
|
|
self.b560 = float(bands_ref[3].item())
|
|
|
|
# Read BRDF LUT and compute default coeffs
|
|
LUT_OCP = xr.open_dataset(adf % 'M02SeaDAS',engine='netcdf4')
|
|
self.LUT = xr.Dataset()
|
|
|
|
# Homogeneise naming convention with other methods... (PZA --> OZA transformation comes below...)
|
|
self.LUT['foq'] = LUT_OCP.f_over_q_LUT.rename({'SZA_FOQ':'theta_s',
|
|
'PZA_FOQ':'theta_v',
|
|
'RAA_FOQ':'delta_phi',
|
|
'log_chl_FOQ':'log_chl_foq'})
|
|
|
|
# Index of refraction
|
|
self.n_w = float(LUT_OCP.water_refraction_index.data)
|
|
|
|
# Remove trivial aot indexation
|
|
self.LUT['foq'] = self.LUT['foq'].squeeze()
|
|
|
|
#
|
|
self.coeffs0 = self.interp_geometries(0., 0., 0.)
|
|
self.coeffs = Coeffs(np.nan)
|
|
|
|
# Parameters for the OC4ME chl retrieval
|
|
self.OC4MEcoeff = LUT_OCP.log10_coeff_LUT.values
|
|
self.OC4MEepsilon = LUT_OCP.oc4me_epsilon
|
|
self.OC4MEchl0 = float(LUT_OCP.oc4me_chl0.values)
|
|
self.niter = LUT_OCP.oc4me_niter
|
|
|
|
""" Initialize pixel: coefficient at current geometry and water IOP at current bands """
|
|
|
|
def init_pixels(self, theta_s, theta_v, delta_phi):
|
|
self.coeffs = self.interp_geometries(theta_s, theta_v, delta_phi)
|
|
|
|
# Cache wavelength and chlorophyll bounds for fast access in forward()
|
|
self.min_wavelength_FOQ = float(np.min(self.coeffs.foq.wavelengths_FOQ))
|
|
self.max_wavelength_FOQ = float(np.max(self.coeffs.foq.wavelengths_FOQ))
|
|
self.min_log_chl_foq = float(np.min(self.coeffs.foq.log_chl_foq) / np.log(10))
|
|
self.max_log_chl_foq = float(np.max(self.coeffs.foq.log_chl_foq) / np.log(10))
|
|
|
|
""" Interpolate coefficients """
|
|
def interp_geometries(self, theta_s, theta_v, delta_phi):
|
|
# Transform PZA to VZA (Snell's refraction Law)
|
|
theta_v = np.rad2deg(np.arcsin(np.sin(np.deg2rad(theta_v)) / self.n_w))
|
|
theta_v_0 = np.clip(theta_v,
|
|
float(np.min(self.LUT.theta_v)),
|
|
float(np.max(self.LUT.theta_v)))
|
|
|
|
foq = self.LUT.foq.interp(theta_s=theta_s, theta_v=theta_v_0, delta_phi=delta_phi)
|
|
|
|
return Coeffs(foq)
|
|
|
|
""" Compute remote-sensing reflectance"""
|
|
|
|
def forward(self, ds, normalized=False):
|
|
|
|
if normalized:
|
|
coeffs = self.coeffs0
|
|
min_wl = float(np.min(coeffs.foq.wavelengths_FOQ))
|
|
max_wl = float(np.max(coeffs.foq.wavelengths_FOQ))
|
|
min_chl = float(np.min(coeffs.foq.log_chl_foq) / np.log(10))
|
|
max_chl = float(np.max(coeffs.foq.log_chl_foq) / np.log(10))
|
|
else:
|
|
coeffs = self.coeffs
|
|
min_wl = self.min_wavelength_FOQ
|
|
max_wl = self.max_wavelength_FOQ
|
|
min_chl = self.min_log_chl_foq
|
|
max_chl = self.max_log_chl_foq
|
|
|
|
wave_foq = np.clip(ds['bands'], min_wl, max_wl)
|
|
log10_chl_foq = np.clip(ds['log10_chl'], min_chl, max_chl)
|
|
|
|
# f/Q LUT indexed with ln(CHL), i.e. log_e(CHL)
|
|
log_chl_foq = log10_chl_foq * np.log(10)
|
|
|
|
# Merged dual interpolation into single operation for better performance
|
|
forward_mod = coeffs.foq.interp(
|
|
wavelengths_FOQ=wave_foq,
|
|
log_chl_foq=log_chl_foq,
|
|
method='linear'
|
|
)
|
|
|
|
return forward_mod
|
|
|
|
""" Apply QAA to retrieve IOP (omega_b, eta_b) from Rrs """
|
|
|
|
def backward(self, ds, iter_brdf):
|
|
|
|
Rrs = ds['nrrs']
|
|
|
|
# Local renaming of bands (pre-converted to float in __init__)
|
|
b442 = self.b442
|
|
b490 = self.b490
|
|
b510 = self.b510
|
|
b560 = self.b560
|
|
|
|
# Apply upper and lower limits to Rrs(665) #TODO check if not finite or missing?
|
|
Rrs442 = Rrs.sel(bands=b442)
|
|
Rrs490 = Rrs.sel(bands=b490)
|
|
Rrs510 = Rrs.sel(bands=b510)
|
|
Rrs560 = Rrs.sel(bands=b560)
|
|
|
|
# Drop unused coordinates to reduce overhead
|
|
Rrs442 = drop_unused_coords(Rrs442)
|
|
Rrs490 = drop_unused_coords(Rrs490)
|
|
Rrs510 = drop_unused_coords(Rrs510)
|
|
Rrs560 = drop_unused_coords(Rrs560)
|
|
|
|
# Compute the OC4ME "R"
|
|
ds['log10_chl_OC4ME_Ratio'] = np.log10(np.max([Rrs442, Rrs490, Rrs510], axis=0) / Rrs560)
|
|
|
|
ds['log10_chl'] = 0 * ds['log10_chl_OC4ME_Ratio']
|
|
# Apply OC4ME 5-degree polynomial using numpy for better performance
|
|
poly = np.polynomial.polynomial.polyval(
|
|
ds['log10_chl_OC4ME_Ratio'].values,
|
|
self.OC4MEcoeff
|
|
)
|
|
ds['log10_chl'].values = poly
|
|
|
|
return ds |