Source code for hgrs.hgrs_kernel

import os
from pkg_resources import resource_filename
import importlib_resources
import yaml

import numpy as np
import pandas as pd
import xarray as xr

from numba import jit
from scipy import ndimage

import matplotlib.pyplot as plt

from multiprocessing import Pool  # Process pool
from multiprocessing import sharedctypes
import itertools
from scipy.optimize import least_squares, minimize
from numba import njit, prange
import logging

from omnicloudmask import predict_from_array

from . import AuxData

opj = os.path.join

configfile = importlib_resources.files(__package__).joinpath('config.yml')
with open(configfile, 'r') as file:
    config = yaml.safe_load(file)

HGRSDATA = config['path']['data_root']
TOALUT = config['path']['toa_lut']
TRANSLUT = config['path']['trans_lut']

LUT_FILE = opj(HGRSDATA, TOALUT)
TRANS_LUT_FILE = opj(HGRSDATA, TRANSLUT)
ABS_GAS_FILE = resource_filename(__package__, 'data/lut/lut_abs_opt_thickness_normalized.nc')
WATER_VAPOR_TRANSMITTANCE_FILE = resource_filename(__package__, 'data/lut/water_vapor_transmittance.nc')


[docs] class Product(): def __init__(self, l1c_obj=None, xcoarsen=20, ycoarsen=20, expon=2): # spectral parameters self.wl_water_vapor = slice(800, 1300) self.wl_sunglint = slice(2150, 2250) # self.wl_atmo = slice(950, 2450) self.wl_atmo = [1000, 1050, 1075, 1100, 1200, 1300, 1600, 1650, 1700, 2150, 2200, 2250] self.wl_non_neg = [430, 490, 560, 650, 750, 800, 865, 1020] self.wl_to_remove = [(935, 967), (1105, 1170), (1320, 1490), (1778, 2033), (2465, 2550)] self.wl_green = slice(540, 570) self.wl_nir = slice(850, 882) self.wl_1600 = slice(1580, 1650) self.wl_rgb = [30, 20, 10] # image chunking and coarsening parameters self.xcoarsen = xcoarsen self.ycoarsen = ycoarsen self.Npix_per_megapix = self.xcoarsen * self.ycoarsen self.block_size = 2 # minimum percentage of water pixel within the mega-pixel to enable processing self.pixel_percentage = 20 self.pixel_threshold = self.pixel_percentage / 100 * self.Npix_per_megapix # number of digits to keep for angle values self.ang_resol = 1 # pre-computed auxiliary data self.lut_file = LUT_FILE self.trans_lut_file = TRANS_LUT_FILE self.abs_gas_file = ABS_GAS_FILE self.water_vapor_transmittance_file = WATER_VAPOR_TRANSMITTANCE_FILE # mask thresholding parameters self.sunglint_threshold = 0.11 self.ndwi_threshold = 0.01 self.green_swir_index_threshold = 0.1 # atmosphere auxiliary data # TODO get them from CAMS self.pressure = 1010 self.to3c = 6.5e-3 self.tno2c = 3e-6 self.tch4c = 1e-2 self.psl = 1013 self.coef_abs_scat = .35 self.altitude = 0 # xarray object to be processed self.raster = l1c_obj.copy() self.fwhm = self.raster.fwhm.reset_coords(drop=True) # .to_dataframe() self.wl = self.raster.wl self.sza_mean = np.nanmean(self.raster.sza) self.vza_mean = np.nanmean(self.raster.vza) self.raa_mean = np.nanmean(self.raster.raa) self.get_air_mass() self.Tg_other = None self.load_auxiliary_data() # spectral function for sensor response convolution # exponent of the super-gaussian spectral response function self.expon = expon # set the convolution module self.spectral = Spectral(self.wl, self.fwhm.values) def load_auxiliary_data(self): # --------------------------------------- # Load pre-computed radiative transfer LUT # --------------------------------------- logging.info('Load pre-computed radiative transfer LUT') # get LUT self.gas_lut = xr.open_dataset(self.abs_gas_file) self.aero_lut = xr.open_dataset(self.lut_file).isel(wind=1) self.Ttot_Ed = xr.open_dataset(self.trans_lut_file).isel(wind=1) self.Twv_lut = xr.open_dataset(self.water_vapor_transmittance_file).interp(wl=self.wl) # convert wavelength in nanometer self.aero_lut['wl'] = self.aero_lut['wl'] * 1000 self.aero_lut['wl'].attrs['description'] = 'wavelength of simulation (nanometer)' self.Ttot_Ed['wl'] = self.Ttot_Ed['wl'] * 1e3 self.Ttot_Ed['wl'].attrs['description'] = 'wavelength of simulation (nanometer)' # get hgrs auxdata self.auxdata = AuxData(self.wl) def get_ndwi(self): green = self.raster.Rtoa.sel(wl=self.wl_green).mean(dim='wl') nir = self.raster.Rtoa.sel(wl=self.wl_nir).mean(dim='wl') self.ndwi = (green - nir) / (green + nir) def get_green_swir_index(self): green = self.raster.Rtoa.sel(wl=self.wl_green).mean(dim='wl') b1600 = self.raster.Rtoa.sel(wl=self.wl_1600).mean(dim='wl') self.green_swir_index = (green - b1600) / (green + b1600) def get_b2200(self): self.b2200 = self.raster.Rtoa.sel(wl=self.wl_sunglint).mean(dim='wl') def apply_water_masks(self): self.get_ndwi() self.get_green_swir_index() self.get_b2200() self.raster['Rtoa'] = self.raster.Rtoa.where(self.ndwi > self.ndwi_threshold). \ where(self.b2200 < self.sunglint_threshold). \ where(self.green_swir_index > self.green_swir_index_threshold).load()
[docs] def get_omnicloudmask(self, rgnir): ''' Apply OmniCloudMAsk for clouds and cloud shadows masking Outputs: 0 = Clear 1 = Thick Cloud 2 = Thin Cloud 3 = Cloud Shadow see https://github.com/DPIRD-DMA/OmniCloudMask refs: Wright, N., Duncan, J. M. A., Callow, J. N., Thompson, S. E., & George, R. J. (2025). Training sensor-agnostic deep learning models for remote sensing: Achieving state-of-the-art cloud and cloud shadow identification with OmniCloudMask. Remote Sensing of Environment, 322, 114694. https://doi.org/10.1016/J.RSE.2025.114694 :param rgnir: raster xarray object with the red, green and nir bands :return omnimask: raster of the retrieved mask ''' pred = predict_from_array(rgnir.fillna(0).values) omnimask = xr.DataArray(pred[0], dims=["y", "x"], coords=dict(x=rgnir.x.values, y=rgnir.y.values, time=rgnir.time, ), attrs=dict( description="OmniCloudMask, see https://github.com/DPIRD-DMA/OmniCloudMask", reference='https://doi.org/10.1016/J.RSE.2025.114694'), ) omnimask.name = 'omnimask' return omnimask
def round_angles(self): for param in ['sza', 'vza', 'raa']: self.raster[param] = self.raster[param].round(self.ang_resol) def get_air_mass(self, raster_name='raster', round=True, digit_resol=3): raster = self.__dict__[raster_name] raster['air_mass'] = 1. / np.cos(np.radians(raster.sza)) + 1. / np.cos(np.radians(raster.vza)) if round: raster['air_mass'] = raster['air_mass'].round(digit_resol) self.air_mass_mean = np.nanmean(raster['air_mass'].values) @staticmethod def remove_wl_dataarray(xarr, wl_to_remove, drop=True): xarr_ = xarr.isel(x=1, y=1) for wls in wl_to_remove: wl_min, wl_max = wls xarr_ = xarr_.where((xarr_.wl < wl_min) | (xarr_.wl > wl_max), drop=drop) wl_final = xarr_.wl.values return xarr.sel(wl=wl_final) @staticmethod def remove_wl_dataset(xds, wl_to_remove, variable='Rtoa', drop=True): xarr_ = xds[variable].isel(x=1, y=1) for wls in wl_to_remove: wl_min, wl_max = wls xarr_ = xarr_.where((xarr_.wl < wl_min) | (xarr_.wl > wl_max), drop=drop) wl_final = xarr_.wl.values return xds.sel(wl=wl_final)
[docs] @staticmethod def Gamma2sigma(Gamma): '''Function to convert FWHM (Gamma) to standard deviation (sigma)''' return Gamma * np.sqrt(2.) / (np.sqrt(2. * np.log(2.)) * 2.)
@staticmethod def gaussian(x, mu, sigma): return 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(-(x - mu) ** 2 / (2 * sigma ** 2))
[docs] @staticmethod @njit(fastmath=True) def super_gaussian(x, amplitude=1.0, mu=0.0, sigma=1.0, expon=10.0): ''' Super-Gaussian distribution: super_gaussian(x, amplitude, mu, sigma, expon) = (amplitude/(sqrt(2*pi)*sigma)) * exp(-abs(x-mu)**expon / (2*sigma**expon)) :param x: :param amplitude: :param mu: :param sigma: :param expon: :return: ''' sigma = max(1.e-15, sigma) return amplitude / (np.sqrt(2 * np.pi) * sigma) * \ np.exp(-np.abs(x - mu) ** expon / (2 * sigma ** expon))
[docs] @staticmethod @njit(fastmath=True) def super_gaussian_fwhm2sigma(fwhm, expon): ''' Function to convert FWHM to standard deviation (sigma) of the super-gaussian distribution :param fwhm: :param expon: :return: ''' return fwhm / 2 * (2 * np.log(2)) ** (-1 / expon)
def plot_rsr(self): wl_ref = np.linspace(360, 2550, 10000) fig, axs = plt.subplots(nrows=1, ncols=1, figsize=(10, 4)) for mu, fwhm in self.fwhm.iterrows(): sig = self.Gamma2sigma(fwhm.values) rsr = self.gaussian(wl_ref, mu, sig) axs.plot(wl_ref, rsr, '-k', lw=0.5, alpha=0.4) axs.set_xlabel('Wavelength (nm)') axs.set_ylabel('Spectral response function') return fig def plot_angles(self, raster_name='raster', figsize=(20, 4), cmap=plt.cm.Spectral_r, **kwargs): raster = self.__dict__[raster_name] params = [raster.sza, raster.vza, raster.raa, raster.air_mass] titles = ['SZA', 'VZA', 'rel. AZI', 'Air mass'] fig, axs = plt.subplots(nrows=1, ncols=4, figsize=figsize) for i, ax in enumerate(axs): params[i].plot.imshow(ax=ax, robust=True, cmap=cmap, **kwargs) ax.set_title(titles[i]) ax.set(xticks=[], yticks=[]) ax.set_ylabel('') ax.set_xlabel('') return fig def plot_params(self, xds, params=['aot_ref', 'aot_ref_std', 'brdfg', 'brdfg_std'], shrink=0.8, cmap=plt.cm.Spectral_r): ncols = len(params) fig_width = ncols * 5 + 2 fig, axs = plt.subplots(1, ncols=ncols, figsize=(fig_width, 4)) axs = axs.ravel() for i in range(4): xds[params[i]].plot.imshow(cmap=cmap, robust=True, vmin=0, # vmax=0.201, cbar_kwargs={'shrink': shrink, 'label': params[i]}, ax=axs[i]) # extent=extent_val, transform=proj, axs[i].set(xticks=[], yticks=[]) axs[i].set_ylabel('') axs[i].set_xlabel('') axs[i].set_title(params[i]) return fig def plot_masks(self, params=['cloud_mask', 'sunglint_mask', 'landcover_mask'], vmax=12, shrink=0.8, cmap=plt.cm.Spectral_r): ncols = len(params) fig_width = ncols * 5 + 1 fig, axs = plt.subplots(1, ncols=ncols, figsize=(fig_width, 4)) fig.subplots_adjust(bottom=0.1, top=0.95, left=0.1, right=0.99, hspace=0.15, wspace=0.15) axs = axs.ravel() for i, param in enumerate(params): self.raster[param].plot.imshow(cmap=cmap, vmax=vmax, robust=True, cbar_kwargs={'shrink': shrink}, ax=axs[i]) # extent=extent_val, transform=proj, axs[i].set(xticks=[], yticks=[]) axs[i].set_ylabel('') axs[i].set_xlabel('') axs[i].set_title(param) return fig def rgb(self, variable='Rtoa', raster_name='raster', gamma=0.5, brightness_factor=1, **kwargs): fig = (self.__dict__[raster_name][variable].isel( wl=self.wl_rgb) ** gamma * brightness_factor).plot.imshow(rgb='wl', robust=True, **kwargs) fig.axes.set(xticks=[], yticks=[]) fig.axes.set_ylabel('') fig.axes.set_xlabel('') return fig def plot_water_pix_number(self, cmap=plt.cm.Spectral_r, **kwargs): try: fig = self.coarse_masked_raster['water_pixel_number'].plot.imshow(cmap=cmap, robust=True, **kwargs) fig.axes.set(xticks=[], yticks=[]) fig.axes.set_ylabel('') fig.axes.set_xlabel('') return fig except: print('please apply algo.get_coarse_masked_raster() before')
[docs] class Algo(Product): def __init__(self, l1c_obj=None, xcoarsen=20, ycoarsen=20, expon=2): Product.__init__(self, l1c_obj, xcoarsen, ycoarsen, expon)
[docs] def get_pressure(self, alt, psl): '''Compute the pressure for a given altitude alt : altitude in meters (float or np.array) psl : pressure at sea level in hPa palt : pressure at the given altitude in hPa''' palt = psl * (1. - 0.0065 * np.nan_to_num(alt) / 288.15) ** 5.255 return palt
def get_coarse_raster(self, variables=['sza', 'vza', 'raa', 'air_mass', 'Rtoa']): self.coarse_raster = self.raster[variables].coarsen(x=self.xcoarsen, y=self.ycoarsen, boundary="pad").mean() def get_coarse_masked_raster(self, variables=['sza', 'vza', 'raa', 'air_mass', 'Rtoa']): self.coarse_masked_raster = self.raster[variables].coarsen(x=self.xcoarsen, y=self.ycoarsen, boundary="pad").mean() self.coarse_masked_raster['water_pixel_number'] = self.raster['Rtoa']. \ isel(wl=slice(10, 20)).mean(dim='wl'). \ coarsen(x=self.xcoarsen, y=self.ycoarsen, boundary="pad").count() def get_gaseous_optical_thickness(self): gas_lut = self.gas_lut ot_o3 = gas_lut.o3 * self.to3c ot_ch4 = gas_lut.ch4 * self.tch4c ot_no2 = gas_lut.no2 * self.tno2c ot_air = (gas_lut.co + self.coef_abs_scat * gas_lut.co2 + self.coef_abs_scat * gas_lut.o2 + self.coef_abs_scat * gas_lut.o4) * self.pressure / 1000 self.abs_gas_opt_thick = ot_ch4 + ot_no2 + ot_o3 + ot_air def get_gaseous_transmittance(self): self.get_gaseous_optical_thickness() wl_ref = self.gas_lut.wl # .values Tg = np.exp(- self.air_mass_mean * self.abs_gas_opt_thick) # TODO check which one better # self.Tg_other = self.spectral.convolve2(Tg,name='Ttot',expon=self.expon) self.Tg_other = self.spectral.convolve(Tg, name='Ttot') # fwhms = self.raster.fwhm.reset_coords(drop=True).to_dataframe() # Tg_int = [] # for mu, fwhm in fwhms.iterrows(): # sig = self.Gamma2sigma(fwhm.values) # rsr = self.gaussian(wl_ref, mu, sig) # Tg_ = (Tg * rsr).integrate('wl') / np.trapezoid(rsr, wl_ref) # Tg_int.append(Tg_.values) # # self.Tg_other = xr.DataArray(Tg_int, name='Ttot', coords={'wl': self.raster.wl.values}) def other_gas_correction(self, raster_name='coarse_masked_raster', variable='Rtoa'): raster = self.__dict__[raster_name] attrs = raster[variable].attrs if attrs.__contains__('other_gas_correction'): if attrs['other_gas_correction']: print('raster ' + raster_name + '.' + variable + ' is already corrected for other gases transmittance') print('set attribute other_gas_correction to False to proceed anyway') return if self.Tg_other is None: self.get_gaseous_transmittance(self.air_mass_mean) raster[variable] = raster[variable] / self.Tg_other raster[variable].attrs['other_gas_correction'] = True
[docs] def water_vapor_correction(self, raster_name='coarse_masked_raster', variable='Rtoa'): ''' :param raster_name: :param variable: :return: ''' raster = self.__dict__[raster_name] attrs = raster[variable].attrs if attrs.__contains__('water_vapor_correction'): if attrs['other_gas_correction']: print('raster ' + raster_name + '.' + variable + ' is already corrected for water vapor transmittance') print('set attribute other_gas_correction to False to proceed anyway') return if self.Twv_raster is None: print('xarray of water vapor transmittance is not set, please run get_wv_transmittance_raster(tcwv_raster)') return raster[variable] = raster[variable] / self.Twv_raster raster[variable].attrs['water_vapor_correction'] = True
def get_wv_transmittance_raster(self, tcwv_raster): tcwv_vals = tcwv_raster.tcwv.round(1) tcwvs = np.unique(tcwv_vals) tcwvs = tcwvs[~np.isnan(tcwvs)] # TODO improve for air_mass raster Twvs = self.Twv_lut.Twv.interp(air_mass=self.air_mass_mean).interp(tcwv=tcwvs, method='linear').drop('air_mass') self.Twv_raster = Twvs.interp(tcwv=tcwv_vals, method='nearest') def get_full_resolution(self, xarr): return xarr.interp(x=self.raster.x, y=self.raster.y)
[docs] class Solver(): def __init__(self): pass
[docs] def errFit(self, hess_inv, resVariance): ''' Error/uncertainty of the estimated parameters :param resVariance: :return: ''' return np.sqrt(np.diag(hess_inv * resVariance))
[docs] def conv_mapping(self, x): """ Nan-mean convolution """ # get index of central pixel idx = len(x) // 2 if np.isnan(x[idx]) and not np.isnan(np.delete(x, idx)).all(): return np.nanmean(np.delete(x, idx)) elif np.isnan(np.delete(x, idx)).all(): return x[idx] else: return np.nanmean(x)
[docs] @staticmethod @jit(nopython=True) def filter2d(image, weight, windows): ''' Function to convolve parameter image with uncertainty image :param image: parameter image :param weight: uncertainty image :param windows: size of the window for convolution :return: convolved result with same shape as image ''' M, N = np.shape(image) Mf, Nf = windows Mf2 = Mf // 2 Nf2 = Nf // 2 threshold = 0 result = image for i in range(M): for j in range(N): num = 0.0 norm = 0.0 if weight[i, j] > threshold: for ii in range(Mf): ix = i - Mf2 + ii if ix < M: for jj in range(Nf): iy = j - Nf2 + jj if iy < N: wgt = weight[ix, iy] if wgt > 0.: num += (wgt * image[ix, iy]) norm += wgt result[i, j] = num / norm return result
[docs] class WaterVapor(Solver): def __init__(self, prod, raster_name='coarse_masked_raster', variable='Rtoa'): self.prod = prod self.raster = prod.__dict__[raster_name] self.air_mass = prod.air_mass_mean # get data for the subset of "water vapor" wavelengths data = self.raster[variable].sel(wl=prod.wl_water_vapor) self.data = data self.nwl, self.height, self.width = data.shape self.x = data.x self.y = data.y self.wl = data.wl # TODO improve to process the air mass raster instead of scalar mean value # TODO check impact of method = 'nearest' self.Twv_ = prod.Twv_lut.Twv.interp(wl=self.data.wl).interp(air_mass=self.air_mass) self.Twv_['wl'] = self.Twv_['wl'] / 1000 self.wl_mic = self.Twv_.wl.values
[docs] def toa_simu(self, wl, Twv, tcwv, a, b): '''wl in micron ''' # print(Twv.tcwv) return Twv.interp(tcwv=tcwv, method='linear').values * (a * wl + b)
def toa_simu2(self, wl, Twv, tcwv, c0, c1, c2, c3): return c0 * np.exp(-c1 * wl ** -c2) * self.Twv_.interp(tcwv=tcwv).values \ + c3 * self.wl_ ** -3 * self.Twv_.interp(tcwv=0.3 * tcwv).values def func(self, x, Twv, wl, y): return self.toa_simu(wl, Twv, *x) - y def func2(self, x, Twv, wl, y): return self.toa_simu2(wl, Twv, *x) - y def solve(self, x0=[2, -0.04, 0.1]): result = np.ctypeslib.as_ctypes(np.full((self.width, self.height, 6), np.nan)) shared_array = sharedctypes.RawArray(result._type_, result) self.x0 = x0 data = self.data height = self.height width = self.width block_size = self.prod.block_size pixel_threshold = self.prod.pixel_threshold if list(self.raster.keys()).__contains__('water_pixel_number'): water_pixel_number = self.raster.water_pixel_number else: water_pixel_number = None global chunk_process def chunk_process(args): window_x, window_y = args tmp = np.ctypeslib.as_array(shared_array) # x0 = [20, -0.04, 0.1] for ix in range(window_x, min(width, window_x + block_size)): for iy in range(window_y, min(height, window_y + block_size)): if water_pixel_number is not None: if water_pixel_number.isel(x=ix, y=iy).values < pixel_threshold: continue y = data.isel(x=ix, y=iy).dropna(dim='wl') # sigma = Rtoa_std.isel(x=ix,y=iy).dropna(dim='wl') # TODO put solver parameter in self instance res_lsq = least_squares(self.func, self.x0, args=(self.Twv_, self.wl_mic, y), bounds=([0, -10, 0], [60, 1, 1]), diff_step=1e-2, xtol=1e-2, ftol=1e-2, max_nfev=20) xres = res_lsq.x resVariance = (res_lsq.fun ** 2).sum() / (len(res_lsq.fun) - len(res_lsq.x)) hess = np.matmul(res_lsq.jac.T, res_lsq.jac) try: hess_inv = np.linalg.inv(hess) std = self.errFit(hess_inv, resVariance) except: std = [np.nan, np.nan, np.nan] tmp[ix, iy, :] = [*xres, *std] return window_idxs = [(i, j) for i, j in itertools.product(range(0, width, block_size), range(0, height, block_size))] p = Pool() res = p.map(chunk_process, window_idxs) result = np.ctypeslib.as_array(shared_array) self.result = result self.water_vapor = xr.Dataset(dict(tcwv=(["y", "x"], result[:, :, 0].T), tcwv_std=(["y", "x"], result[:, :, 3].T)), coords=dict( x=self.x, y=self.y), attrs=dict( description="Fitted Total Columnar Water vapor; warning for transmittance computation only", units="kg/m**2") )
[docs] class Aerosol(Solver): def __init__(self, prod, aerosol_model='COAV_rh70', first_guess=[0.01, 0], aot550_limits=[0.002, 0.8], raster_name='coarse_masked_raster', variable='Rtoa'): self.prod = prod self.aerosol_model = aerosol_model self.raster = prod.__dict__[raster_name] self.auxdata = prod.auxdata self.aero_lut = prod.aero_lut # set box limits in aod550 for non-linear optimization self.aod550_min = aot550_limits[0] self.aod550_max = aot550_limits[1] self.first_guess = first_guess # get full resolution parameters self.xfull = prod.raster.x self.yfull = prod.raster.y # get data for the subset of "black water" wavelengths self.data = self.raster[variable] data = self.data self.wl_atmo = prod.wl_atmo self.wl_non_neg = prod.wl_non_neg self.nwl, self.height, self.width = data.shape self.x = data.x self.y = data.y self.wl = data.wl self.sza = prod.sza_mean self.vza = prod.vza_mean self.raa = prod.raa_mean self.raa_lut = (180 - self.raa) % 360 self.air_mass = prod.air_mass_mean self.pressure = prod.pressure # process parameters self.block_size = self.prod.block_size self.pixel_threshold = self.prod.pixel_threshold self.wl_sunglint = self.prod.wl_sunglint self.prepare_lut(self.wl) def prepare_lut(self, wl): auxdata = self.auxdata sza = self.sza vza = self.vza raa_lut = self.raa_lut self.sunglint_eps = auxdata.sunglint_eps.interp(wl=wl) self.rot = auxdata.rot.interp(wl=wl) * self.pressure / self.auxdata.pressure_rot_ref aot_refs = [0, *np.logspace(-3, np.log10(0.8), 100)] self.aot_lut = self.aero_lut.sel(model=self.aerosol_model).aot.interp(wl=wl, method='quadratic').interp( aot_ref=aot_refs, method='quadratic').dropna('aot_ref') # sel(wl=wl_glint) norm_radiance = self.aero_lut.sel(model=self.aerosol_model ).I.interp(vza=vza, azi=raa_lut, method='linear' ).interp(sza=sza, method='quadratic').squeeze() self.Rtoa_lut = norm_radiance.interp(wl=wl, method='quadratic') \ .interp(aot_ref=aot_refs, method='quadratic').dropna('aot_ref') / np.cos(np.radians(sza)) def transmittance_dir(self, aot, M, rot=0): return np.exp(-(rot + aot) * M)
[docs] def toa_simu(self, aot, rot, Rtoa_lut, sunglint_eps, aot_ref, BRDFg): ''' ''' aot = aot.interp(aot_ref=aot_ref) Rdiff = Rtoa_lut.interp(aot_ref=aot_ref) Tdir = self.transmittance_dir(aot, self.air_mass, rot=rot) sunglint_corr = Tdir * sunglint_eps Rdir = sunglint_corr * BRDFg / (Tdir.sel(wl=self.wl_sunglint) * sunglint_eps.sel(wl=self.wl_sunglint)).mean( dim='wl') # sunglint_toa.Rtoa.plot(x='wl',hue='aot_ref',ax=axs[0]) return Rdiff + Rdir
def func(self, x, aot, rot, Rtoa_lut, sunglint_eps, y): return (y - self.toa_simu(aot, rot, Rtoa_lut, sunglint_eps, *x)) # /sigma def cost_func(self, x, aot, rot, Rtoa_lut, sunglint_eps, y): return np.sum((self.func(x, aot, rot, Rtoa_lut, sunglint_eps, y) ** 2)) def constraint(self, x, aot, rot, Rtoa_lut, sunglint_eps, y): return np.min((self.func(x, aot, rot, Rtoa_lut, sunglint_eps, y))) def solve(self, x0=[0.005, 0.]): result = np.ctypeslib.as_ctypes(np.full((self.width, self.height, 4), np.nan)) shared_array = sharedctypes.RawArray(result._type_, result) # TODO clean up method to assign first guess self.x0 = x0 self.x0 = self.first_guess data = self.data height = self.height width = self.width if list(self.raster.keys()).__contains__('water_pixel_number'): water_pixel_number = self.raster.water_pixel_number else: water_pixel_number = None global chunk_process def chunk_process(args): window_x, window_y = args tmp = np.ctypeslib.as_array(shared_array) x0 = self.x0 for ix in range(window_x, min(width, window_x + self.block_size)): for iy in range(window_y, min(height, window_y + self.block_size)): if water_pixel_number is not None: if water_pixel_number.isel(x=ix, y=iy).values < self.pixel_threshold: continue # x0 = self.x0 yfull = data.isel(x=ix, y=iy).dropna(dim='wl') # sigma = Rtoa_std.isel(x=ix,y=iy).dropna(dim='wl') cons = ({'type': 'ineq', 'fun': self.constraint, 'args': (self.aot_lut, self.rot, self.Rtoa_lut, self.sunglint_eps, yfull.sel(wl=self.wl_non_neg, method='nearest')) }) min_res = minimize(self.cost_func, x0, args=(self.aot_lut, self.rot, self.Rtoa_lut, self.sunglint_eps, yfull.sel(wl=self.wl_atmo, method='nearest')), method='SLSQP', bounds=((self.aod550_min, self.aod550_max), (0, 1.3)), constraints=cons, options={'maxiter': 10} ) xres = min_res.x if min_res.success: x0 = xres # except: # print(wl_,aot_,rot_,Rtoa_lut_,sunglint_eps_, y) # break std = [min_res.fun, np.sum(min_res.jac ** 2)] tmp[ix, iy, :] = [*xres, *std] window_idxs = [(i, j) for i, j in itertools.product(range(0, self.width, self.block_size), range(0, self.height, self.block_size))] p = Pool() res = p.map(chunk_process, window_idxs) result = np.ctypeslib.as_array(shared_array) self.aero_img = xr.Dataset(dict(aot_ref=(["y", "x"], result[:, :, 0].T), brdfg=(["y", "x"], result[:, :, 1].T), aot_ref_std=(["y", "x"], result[:, :, 2].T), brdfg_std=(["y", "x"], result[:, :, 3].T) ), coords=dict( x=self.x, y=self.y), attrs=dict( description="aerosol and sunglint retrieval from coarse resolution data", aerosol_model=self.aerosol_model) ) def smoothing(self, weights, windows=np.array([1, 1]), mask=np.ones((3, 3)) ): #weights = (1 / self.aero_img['aot_ref_std'] ** 2).__deepcopy__().to_numpy().astype(float) param = self.aero_img['aot_ref'].__deepcopy__().to_numpy().astype(float) aot_ref_smoothed = self.filter2d(param, weights, windows) res = ndimage.generic_filter(aot_ref_smoothed, function=self.conv_mapping, footprint=mask, mode='nearest') self.aero_img['aot_ref_smoothed'] = xr.DataArray(res, coords=dict(y=self.aero_img.y, x=self.aero_img.x)) def get_aot_full_resolution(self): # TODO change fill_value (extrapolate is not safe) with median of retrieval, for instance self.aot_ref_full = \ self.aero_img['aot_ref_smoothed'].interp(x=self.xfull, y=self.yfull, method='linear', kwargs={"fill_value": "extrapolate"}) def get_atmo_parameters(self, prod): wl = prod.coarse_masked_raster.wl weights = prod.coarse_masked_raster['water_pixel_number'].__deepcopy__().to_numpy().astype(float) # get LUT for desired wavelengths self.prepare_lut(wl) self.smoothing(weights) self.get_aot_full_resolution() # construct aot raster aot_ref_median = self.aero_img.aot_ref_smoothed.median() aot_ref_vals = self.aero_img['aot_ref_smoothed'].fillna(aot_ref_median).round(3) aot_refs = np.unique(aot_ref_vals) aot_refs = aot_refs[~np.isnan(aot_refs)] # TODO update LUT for aot< 0.001 aot_refs[aot_refs < 0.002] = 0.002 # if rounded aot_ref has unique value if len(aot_refs) == 1: aot_refs = np.concatenate([aot_refs, 1.2 * aot_refs]) aots = self.aot_lut.interp(aot_ref=aot_refs, method='linear') aots = aots.interp(aot_ref=aot_ref_vals, method='nearest') aots.name = 'aot' aots.attrs['description'] = 'spectral aerosol optical thickness' # construct raster for diffuse atmospheric reflectance Rdiffs = self.Rtoa_lut.interp(aot_ref=aot_refs, method='linear') Rdiffs = Rdiffs.interp(aot_ref=aot_ref_vals, method='nearest') Rdiffs.name = 'Rtoa_diff' Rdiffs.attrs['description'] = 'top-of-atmosphere atmosphere reflectance' # construct raster for direct transmittance due to rayleigh and aerosol Tdirs = self.transmittance_dir(aots, self.air_mass, rot=self.rot) Tdirs.name = 'Tdir' Tdirs.attrs['description'] = 'direct transmittance due to rayleigh and aerosol for total air mass' # merge into dataset self.atmo_img = xr.merge([aots, Rdiffs, Tdirs]) self.atmo_img.attrs['description'] = "atmospheric parameters for rayleigh and aerosol components", self.atmo_img.attrs['aerosol_model'] = self.aerosol_model
[docs] @njit(fastmath=True) def Gamma2sigma(Gamma): '''Function to convert FWHM (Gamma) to standard deviation (sigma)''' return Gamma * np.sqrt(2.) / (np.sqrt(2. * np.log(2.)) * 2.)
[docs] @njit(fastmath=True) def gaussian(x, mu, sigma): return 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(-(x - mu) ** 2 / (2 * sigma ** 2))
[docs] @njit(fastmath=True) def super_gaussian(x, amplitude=1.0, mu=0.0, sigma=1.0, expon=10.0): ''' Super-Gaussian distribution: super_gaussian(x, amplitude, mu, sigma, expon) = (amplitude/(sqrt(2*pi)*sigma)) * exp(-abs(x-mu)**expon / (2*sigma**expon)) :param x: :param amplitude: :param mu: :param sigma: :param expon: :return: ''' sigma = max(1.e-15, sigma) return amplitude / (np.sqrt(2 * np.pi) * sigma) * \ np.exp(-np.abs(x - mu) ** expon / (2 * sigma ** expon))
[docs] @njit(fastmath=True) def super_gaussian_fwhm2sigma(fwhm, expon): ''' Function to convert FWHM to standard deviation (sigma) of the super-gaussian distribution :param fwhm: :param expon: :return: ''' return fwhm / 2 * (2 * np.log(2)) ** (-1 / expon)
[docs] class Spectral(): def __init__(self, central_wl, fwhm): ''' Convolve with spectral response of sensor based on full width at half maximum of each band :param central_wl: numpy array of the central wavelengths :param fwhm: scalar or numpy array containing full width at half maximum in nm :param info: optional parameter to feed the attributes of the output xarray :return: ''' self.central_wl = central_wl if not isinstance(fwhm, np.ndarray): fwhm = np.array([fwhm] * len(central_wl)) fwhm = xr.DataArray(fwhm, name='fwhm', coords={'wl': central_wl}, attrs={ 'definition': 'full width at half maximum of spectral responses modeled as gaussian distributions'}) self.fwhm = fwhm def plot_rsr(self): wl_ref = np.linspace(360, 2550, 10000) fig, axs = plt.subplots(nrows=1, ncols=1, figsize=(10, 4)) for mu, fwhm in self.fwhm.groupby('wl'): sig = self.Gamma2sigma(fwhm.values) rsr = self.gaussian(wl_ref, mu, sig) axs.plot(wl_ref, rsr, '-k', lw=0.5, alpha=0.4) axs.set_xlabel('Wavelength (nm)') axs.set_ylabel('Spectral response function') return fig
[docs] @staticmethod @njit(parallel=True) def convolve_( wl_signal, signal, wl, fwhm, ): ''' Convolution assuming Dirac for signal source spectral response :paral wl_signal: wavelength array of spectral signal :param signal: numpy of signal to convolve, coord=wl_signal :param wl: numpy of wavelength coordinates of signal :param fwhm: numpy with data=fwhm containing full width at half maximum in nm :return: numpy of convoluted signal ''' Nwl = len(wl) signal_ = np.full((Nwl), np.nan, dtype=np.float32) for ii in prange(len(fwhm)): sig = Gamma2sigma(fwhm[ii]) rsr = gaussian(wl_signal, wl[ii], sig) signal_[ii] = np.trapezoid((signal * rsr), wl_signal) / np.trapezoid(rsr, wl_signal) return signal_
[docs] @staticmethod @njit(parallel=True) def convolve2_( wl_signal, signal, wl, fwhm, expon=2., threshold=1e-6 ): ''' Convolution assuming Dirac for signal source spectral response :paral wl_signal: wavelength array of spectral signal :param signal: numpy of signal to convolve, coord=wl_signal :param wl: numpy of wavelength coordinates of signal :param fwhm: numpy with data=fwhm containing full width at half maximum in nm :param threshold: minimum values of the response function to be included in the convolution :return: numpy of convoluted signal ''' Nwl = len(wl) response = np.full((Nwl), np.nan, dtype=np.float32) for ii in prange(len(fwhm)): sig = super_gaussian_fwhm2sigma(fwhm[ii], expon) rsr = super_gaussian(wl_signal, mu=wl[ii], sigma=sig, expon=expon) # remove values above a given threshold to speed up computation idx = rsr > threshold wl_signal_ = wl_signal[idx] signal_ = signal[idx] rsr = rsr[idx] response[ii] = np.trapezoid((signal_ * rsr), wl_signal_) / np.trapezoid(rsr, wl_signal_) return response
[docs] def convolve2(self, signal, name='signal', expon=3, threshold=1e-4, info={}): ''' Convolve with spectral response of sensor based on full width at half maximum of each band :param signal: xarray spectral signal to convolve, coord=wl :param fwhm: xarray with data=fwhm containing full width at half maximum in nm, and coords=wl :param info: optional parameter to feed the attributes of the output xarray :param threshold: minimum values of the response function to be included in the convolution :return: ''' wl_ref = signal.wl.values fwhm = self.fwhm.values wl = self.fwhm.wl.values xdims = signal.dims attrs = signal.attrs name = signal.name if len(xdims) == 1: signal_int = self.convolve2_(wl_ref, signal.values, wl, fwhm, expon, threshold=threshold) signal_int = xr.DataArray(signal_int, name=name, coords={'wl': self.fwhm.wl.values}, attrs=attrs) else: # to handle multidimensional xarray xdims = np.array(xdims) xdims = xdims[xdims != 'wl'] xsignal_int = [] for dim in xdims: xsignal_int_ = [] for value, signal_ in signal.groupby(dim): # print(dim, value) signal_ = signal_.squeeze() _ = self.convolve2_(signal_.wl.values, signal_.values, wl, fwhm, expon) _ = xr.Dataset({name: (['wl'], _)}, coords={'wl': wl, dim: value}) xsignal_int_.append(_) xsignal_int.append(xr.concat(xsignal_int_, dim=dim)) signal_int = xr.merge(xsignal_int) # .to_dataarray() signal_int.attrs = attrs return signal_int
[docs] def convolve(self, signal, name='signal', info={}): ''' Convolve with spectral response of sensor based on full width at half maximum of each band :param signal: xarray spectral signal to convolve, coord=wl :param fwhm: xarray with data=fwhm containing full width at half maximum in nm, and coords=wl :param info: optional parameter to feed the attributes of the output xarray :return: ''' wl_ref = signal.wl.values fwhm = self.fwhm.values wl = self.fwhm.wl.values xdims = signal.dims if len(xdims) == 1: signal_int = self.convolve_(wl_ref, signal.values, wl, fwhm) signal_int = xr.DataArray(signal_int, name=name, coords={'wl': self.fwhm.wl.values}, attrs=info) else: # to handle multidimensional xarray xdims = np.array(xdims) xdims = xdims[xdims != 'wl'] xsignal_int = [] for dim in xdims: xsignal_int_ = [] for value, signal_ in signal.groupby(dim): # print(dim, value) signal_ = signal_.squeeze() _ = self.convolve_(signal_.wl.values, signal_.values, wl, fwhm) _ = xr.Dataset({name: (['wl'], _)}, coords={'wl': wl, dim: value}) xsignal_int_.append(_) xsignal_int.append(xr.concat(xsignal_int_, dim=dim)) signal_int = xr.merge(xsignal_int).to_dataarray() signal_int.attrs = info return signal_int