Source code for hgrs.hgrs_process

# python

import os, copy

import importlib_resources
import yaml
import glob

from tqdm.auto import tqdm

import numpy as np
import scipy.optimize as so
import pandas as pd
import xarray as xr

import datetime as dt
import logging

import hgrs.driver as driver
import hgrs

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)


[docs] class Process(): def __init__(self): self.lut_file = opj(HGRSDATA, TOALUT) self.trans_lut_file = opj(HGRSDATA, TRANSLUT) # self.cams_dir = CAMS_PATH # self.Nproc = NCPU self.pressure_ref = 101500. self.flags_tokeep = [3] self.flags_tomask = [0, 1, 10, 13, 14, 18] self.successful = False def execute(self, img_path, cams_path ): # --------------------------------------- # construct L1C image plus angle rasters # --------------------------------------- logging.info('construct L1C image plus angle rasters') # action = 'load L1C image plus angle rasters' # pbar = tqdm(total=len(action), # desc=action + f": {img_path} ") if isinstance(img_path, str): logging.info('Opening EnMAP image') try: driver = hgrs.Driver('enmap') l1_prod = driver.read_l1c_enmap(img_path, reflectance_unit=True) except: logging.info('input file format not recognized, stop') return else: logging.info('Opening PRISMA image') print(img_path) try: driver = hgrs.Driver('prisma') l1_prod = driver.read_prisma(img_path[0], img_path[1], reflectance_unit=True, drop_vars=True) except: logging.info('input file format not recognized, stop') return # get L1C object self.l1_prod = l1_prod date = l1_prod.time raster = l1_prod.sza.rio.reproject(4326) clon, clat = float(raster.x.mean()), float(raster.y.mean()) # pbar.refresh() # ----------------------------------------- # Create hGRS object # ----------------------------------------- logging.info('Create hGRS object') prod = hgrs.Algo(l1_prod, xcoarsen=20, ycoarsen=20) prod.round_angles() # ----------------------------------------- # get CAMS and set atmospheric parameters # ----------------------------------------- logging.info('get CAMS and set atmospheric parameters') # lazy loading cams = xr.open_dataset(cams_path, decode_cf=True, chunks={'time': 1, 'x': 500, 'y': 500}) # fix for new ADS format (sept 2024) if ('forecast_period' in cams.dims) & ('forecast_reference_time' in cams.dims): cams = cams.stack(time_buffer=['forecast_period', 'forecast_reference_time']).swap_dims( {'time_buffer': 'valid_time'}).sortby('valid_time').rename( {'valid_time': 'time'}).drop_vars(['time_buffer']) # slicing cams = cams.sel(time=date, method='nearest') cams = cams.sel(latitude=clat, longitude=clon, method='nearest') # select OPAC aerosol model # aod = cams[['aod355', 'aod380', 'aod400', 'aod440', 'aod469', 'aod500', 'aod550', 'aod645', 'aod670', # 'aod800', 'aod865', 'aod1020', 'aod1064', 'aod1240', 'aod1640', 'aod2130']].to_pandas() # aod.index = aod.index.str.replace('aod', '').astype(int) # cams_aod = aod.to_xarray().rename({'index': 'wl'}) cams_wls = [469, 550, 670, 865, 1240] param_aod = [] for wl in cams_wls: wl_ = str(wl) param_aod.append('aod' + wl_) cams_aod = cams[param_aod].to_array(dim='wl') wl_cams = cams_aod.wl.str.replace('aod', '').astype(float) cams_aod = cams_aod.assign_coords(wl=wl_cams) # new LUT: lut_aod = prod.aero_lut.aot.sel(aot_ref=1).interp(wl=cams_aod.wl) idx = np.abs((cams_aod / cams.aod550) - lut_aod).sum('wl').argmin() opac_model = prod.aero_lut.model.values[idx] logging.info('OPAC model: ' + opac_model) # set gases and pressure prod.pressure = float(cams.sp) * 1e-2 prod.to3c = float(cams.gtco3) prod.tno2c = float(cams.tcno2) prod.tch4c = float(cams.tc_ch4) # ----------------------------------------- # Apply cloud, water masking # ----------------------------------------- # TODO put omnimask settins (bands) in config.yml logging.info('Apply omnicloudmask') red_index = 670 green_index = 550 nir_index = 940 rgnir = prod.raster.Rtoa.sel(wl=[red_index, green_index, nir_index], method='nearest').fillna(0) # .values omnimask = prod.get_omnicloudmask(rgnir) prod.raster['Rtoa'] = prod.raster['Rtoa'].where(omnimask == 0) logging.info('Apply water masking') prod.apply_water_masks() # ----------------------------------------- # Construct coarse resolution raster # ----------------------------------------- logging.info('Construct coarse resolution raster') prod.get_coarse_masked_raster() # prod.plot_water_pix_number() # ----------------------------------------- # Correct for gaseous absorption # ----------------------------------------- logging.info('Correct for gaseous absorption') prod.get_gaseous_transmittance() prod.other_gas_correction() # ------------------------------------------ # water vapor retrieval and correction # ------------------------------------------ logging.info('water vapor retrieval and correction') wv_retrieval = hgrs.WaterVapor(prod) wv_retrieval.solve() prod.get_wv_transmittance_raster(wv_retrieval.water_vapor) prod.water_vapor_correction() logging.info('mask bands where gaseous abs. is too strong') Tg_tot = prod.Tg_other * prod.Twv_raster.mean(['x', 'y']) # ------------------------------------------ # aerosol retrieval # ------------------------------------------ logging.info('aerosol retrieval') variable = 'Rtoa' # prod.coarse_masked_raster = prod.remove_wl_dataset( # prod.coarse_masked_raster, prod.wl_to_remove, variable=variable) prod.coarse_masked_raster = prod.remove_wl_dataset( prod.coarse_masked_raster, prod.wl_to_remove, variable=variable) # remove bands where Tg is below a threshold (typically Tg < 0.5) prod.coarse_masked_raster[variable] = prod.coarse_masked_raster[variable].where(Tg_tot > 0.5, drop=True) prod.raster[variable] = prod.raster[variable].where(Tg_tot > 0.5, drop=True) # TODO double check regularization from CAMS AOT values aod550_mean = cams.aod550.mean().values aod550_std = cams.aod550.std().values aod550_std = np.max([aod550_std, 0.2 * aod550_mean + 0.05]) aot550_min = 0.002 # np.max([aod550_mean - 2*aod550_std,0.001]) aero_retrieval = hgrs.Aerosol(prod, aerosol_model=opac_model, first_guess=[aod550_mean, 0.], aot550_limits=[aot550_min, aod550_mean + 2 * aod550_std]) aero_retrieval.solve() aero_retrieval.prepare_lut(prod.coarse_masked_raster.wl) weights = prod.coarse_masked_raster['water_pixel_number'].__deepcopy__().to_numpy().astype(float) aero_retrieval.smoothing(weights) # self.aero_img['aot_ref_smoothed']=self.aero_img['aot_ref_smoothed']*0.5 # construct aot raster # with full res # aot_ref_vals = self.aot_ref_full.round(3) # with coarse res aot_ref_vals = aero_retrieval.aero_img['aot_ref_smoothed'].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]) print(aot_refs) aots = aero_retrieval.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 # TODO check quadratic interpolation (should be much better) Rdiffs = aero_retrieval.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 = aero_retrieval.transmittance_dir(aots, aero_retrieval.air_mass, rot=aero_retrieval.rot) Tdirs.name = 'Tdir' Tdirs.attrs['description'] = 'direct transmittance due to rayleigh and aerosol for total air mass' aero_retrieval.get_atmo_parameters(prod) self.aero_retrieval = aero_retrieval # ------------------------------------------ # full resolution processing # ------------------------------------------ logging.info('process full resolution') prod.raster = prod.remove_wl_dataset(prod.raster, prod.wl_to_remove) prod.other_gas_correction(raster_name='raster', variable='Rtoa') # ------------------------------------------ # water vapor # ------------------------------------------ chunk = 256 height, width, Nwl = len(prod.raster.y), len(prod.raster.x), len(prod.raster.wl) results = np.full((height, width), 0, dtype=np.float32) variable = 'Rtoa' for iy in range(0, height, chunk): yc = min(height, iy + chunk) if yc > height: continue for ix in range(0, width, chunk): xc = min(width, ix + chunk) if xc > width: continue raster = prod.raster[variable][:, iy:yc, ix:xc] Twv_raster = prod.Twv_raster.interp(x=raster.x, y=raster.y) prod.raster[variable].data[:, iy:yc, ix:xc] = raster / Twv_raster Rdiff_full = aero_retrieval.atmo_img.Rtoa_diff # .interp(x=prod.raster.x, y=prod.raster.y) Tdir_full = aero_retrieval.atmo_img.Tdir # .interp(x=prod.raster.x, y=prod.raster.y) wl_sunglint = prod.wl_sunglint Rrs = np.full((Nwl, height, width), np.nan, dtype=np.float32) BRDF_sunglint = np.full((height, width), np.nan, dtype=np.float32) for iy in range(0, height, chunk): yc = min(height, iy + chunk) if yc > height: continue for ix in range(0, width, chunk): xc = min(width, ix + chunk) if xc > width: continue Rcorr = prod.raster.Rtoa[:, iy:yc, ix:xc] Rdiff_full_ = Rdiff_full.interp(x=Rcorr.x, y=Rcorr.y) Rcorr = Rcorr - Rdiff_full_ Tdir_full_ = Tdir_full.interp(x=Rcorr.x, y=Rcorr.y) sunglint_eps = aero_retrieval.sunglint_eps BRDF_sunglint[iy:yc, ix:xc] = (Rcorr.sel(wl=wl_sunglint) / (Tdir_full_.sel(wl=wl_sunglint) * sunglint_eps.sel(wl=wl_sunglint))).mean( dim='wl') # TODO clean up xarray inheritance of some extra coordinates... # BRDF_sunglint = BRDF_sunglint.drop_vars('aot_ref', errors=False).squeeze() Rdir = Tdir_full_ * sunglint_eps * BRDF_sunglint[iy:yc, ix:xc] Rrs[:, iy:yc, ix:xc] = (Rcorr - Rdir) / np.pi l2_prod = xr.Dataset(dict(Rrs=(["wl", "y", "x"], Rrs), brdfg_full=(["y", "x"], BRDF_sunglint), ), coords=dict(x=prod.raster.x, y=prod.raster.y, wl=prod.raster.wl), ) # finally correct for down and upward transmittances # TODO compute pixel wise Ttot_Ed = xr.open_dataset(TRANS_LUT_FILE).isel(wind=1) Ttot_Ed['wl'] = Ttot_Ed['wl'] * 1e3 aot_ref = float(aero_retrieval.aero_img.aot_ref.mean()) wl = l2_prod.Rrs.wl.values sza = float(aero_retrieval.sza) vza = float(aero_retrieval.vza) Ttot_Ed_ = Ttot_Ed.Ttot_Ed.sel(model=opac_model).interp(sza=sza, method='cubic' ).interp(aot_ref=aot_ref, method='quadratic').interp( wl=wl, method='cubic') Ttot_Lu_ = Ttot_Ed.Ttot_Ed.sel(model=opac_model).interp(sza=vza, method='cubic' ).interp(aot_ref=aot_ref, method='quadratic').interp( wl=wl, method='cubic') ** 1.05 Ttot = (Ttot_Ed_ * Ttot_Lu_).reset_coords(drop=True) l2_prod['Rrs'] = l2_prod.Rrs / Ttot # ----------------------------- # construct output image # ----------------------------- logging.info('construct final product') # ----------------------------- # data wv = wv_retrieval.water_vapor.rename({"x": "xc", "y": "yc"}) aero = aero_retrieval.aero_img.rename({"x": "xc", "y": "yc"}) water_pixel_prop = (prod.coarse_masked_raster.water_pixel_number / prod.Npix_per_megapix).drop_vars( 'tcwv').rename({"x": "xc", "y": "yc"}) water_pixel_prop.name = 'water_pix_prop' # geom = prod.raster[['lon', 'lat']].drop_vars('tcwv') # Rrs_ = Rrs_l2.reset_coords().drop_vars(['model', 'z']).rename({'tcwv': 'tcwv_full', 'aot_ref': 'aot_ref_full'}).set_coords(['time','spatial_ref']) l2_prod = xr.merge([l2_prod, wv, aero, water_pixel_prop]) # l2_prod['brdfg_full'] = BRDF_sunglint param = 'Rrs' l2_prod[param].attrs['unit'] = 'per steradian' l2_prod[param].attrs['long_name'] = 'Remote sensing reflectance' l2_prod[param].attrs['description'] = 'Directional water-leaving radiance normalized ' + \ 'by downwelling irradiance in the observation geometry' param = 'water_pix_prop' l2_prod[param].attrs['unit'] = '-' l2_prod[param].attrs['description'] = 'Relative number of water pixel within mega-pixel used for inversion' param = 'brdfg' l2_prod[param].attrs['unit'] = '-' l2_prod[param].attrs['long_name'] = 'BRDF_sunglint' l2_prod[param].attrs['description'] = 'Bidirectional reflectance distribution function ' + \ 'estimated from the sunglint in the SWIR for the observation geometry' param = 'brdfg_std' l2_prod[param].attrs['unit'] = '-' l2_prod[param].attrs['long_name'] = 'BRDF_sunglint_standard deviation' l2_prod[param].attrs['description'] = 'Uncertainty based on optimal estimation procedure' param = 'brdfg_full' l2_prod[param].attrs['unit'] = '-' l2_prod[param].attrs['long_name'] = 'BRDF_sunglint' l2_prod[param].attrs['description'] = 'Bidirectional reflectance distribution function ' + \ 'estimated from the sunglint in the SWIR for the observation geometry' param = 'aot_ref' l2_prod[param].attrs['unit'] = '-' l2_prod[param].attrs['long_name'] = 'aerosol_optical_thickness' l2_prod[param].attrs['description'] = 'Aerosol optical thickness at the reference wavelength (550nm)' param = 'aot_ref_std' l2_prod[param].attrs['unit'] = '-' l2_prod[param].attrs['long_name'] = 'aerosol_optical_thickness_standard_deviation' l2_prod[param].attrs['description'] = 'Uncertainty based on optimal estimation procedure' # param = 'aot_ref_full' # l2_prod[param].attrs['unit'] = '-' # l2_prod[param].attrs['long_name'] = 'aerosol_optical_thickness' # l2_prod[param].attrs['description'] = 'Aerosol optical thickness at the reference wavelength (550nm)' param = 'tcwv' l2_prod[param].attrs['unit'] = 'kg m-2' l2_prod[param].attrs['long_name'] = 'total_columnar_water_vapor' l2_prod[param].attrs['description'] = 'Water vapor integrated over the atmospheric layer' param = 'tcwv_std' l2_prod[param].attrs['unit'] = 'kg m-2' l2_prod[param].attrs['long_name'] = 'total_columnar_water_vapor_standard_deviation' l2_prod[param].attrs['description'] = 'Uncertainty based on optimal estimation procedure' # param = 'tcwv_full' # l2_prod[param].attrs['unit'] = 'kg m-2' # l2_prod[param].attrs['long_name'] = 'total_columnar_water_vapor' # l2_prod[param].attrs['description'] = 'Water vapor integrated over the atmospheric layer' l2_prod['pressure'] = prod.pressure l2_prod['pressure'].attrs['unit'] = 'hPa' l2_prod['pressure'].attrs['description'] = 'Atmospheric pressure at the surface level' l2_prod['pressure'].attrs['source'] = 'computed from CAMS and DEM (see DEM metadata)' param = 'to3c' l2_prod[param] = prod.__dict__[param] l2_prod[param].attrs['unit'] = '' l2_prod[param].attrs['description'] = 'Total columnar ozone concentration' l2_prod[param].attrs['source'] = 'CAMS' param = 'tno2c' l2_prod[param] = prod.__dict__[param] l2_prod[param].attrs['unit'] = '' l2_prod[param].attrs['description'] = 'Total columnar Nitrogen dioxide concentration' l2_prod[param].attrs['source'] = 'CAMS' # ----------------------------- # --metadata l2_prod.attrs = prod.raster.attrs l2_prod.attrs['processing_date'] = str(dt.datetime.now()) l2_prod.attrs['acquisition_date'] = str(l2_prod.attrs['acquisition_date']) l2_prod.attrs['hgrs_version'] = hgrs.__version__ l2_prod.attrs['description'] = 'PRISMA L2A-hGRS cube data' l2_prod.attrs['DEM'] = 'not available' l2_prod.attrs['aerosol_model'] = aero_retrieval.aerosol_model keys = ['wl_water_vapor', 'wl_sunglint', 'wl_atmo', 'wl_to_remove', 'wl_non_neg', 'wl_green', 'wl_nir', 'wl_1600', 'wl_rgb', 'xcoarsen', 'ycoarsen', 'Npix_per_megapix', 'block_size', 'pixel_percentage', 'pixel_threshold', 'ang_resol', 'abs_gas_file', 'lut_file', 'water_vapor_transmittance_file', 'sunglint_threshold', 'ndwi_threshold', 'green_swir_index_threshold', 'pressure', 'to3c', 'tno2c', 'tch4c', 'psl', 'coef_abs_scat', 'altitude'] for key in keys: print(key) l2_prod.attrs[key] = str(prod.__dict__[key]) self.l2_prod = l2_prod self.successful = True return def write_output(self, ofile): ###################################### # Write final product ###################################### logging.info('export final product into netcdf') complevel = 5 encoding = { 'Rrs': {'dtype': 'int16', 'scale_factor': 0.00001, 'add_offset': .2, '_FillValue': -32768, "zlib": True, "complevel": complevel}, # 'aot_ref_full': {'dtype': 'int16', 'scale_factor': 0.001, '_FillValue': -9999, "zlib": True, # "complevel": complevel}, 'aot_ref': {'dtype': 'int16', 'scale_factor': 0.001, '_FillValue': -9999, "zlib": True, "complevel": complevel}, 'aot_ref_std': {'dtype': 'int16', 'scale_factor': 0.001, '_FillValue': -9999, "zlib": True, "complevel": complevel}, 'brdfg_full': {'dtype': 'int16', 'scale_factor': 0.00001, 'add_offset': .2, '_FillValue': -32768, "zlib": True, "complevel": complevel}, 'brdfg': {'dtype': 'int16', 'scale_factor': 0.00001, 'add_offset': .2, '_FillValue': -32768, "zlib": True, "complevel": complevel}, 'brdfg_std': {'dtype': 'int16', 'scale_factor': 0.00001, 'add_offset': .2, '_FillValue': -32768, "zlib": True, "complevel": complevel}, # 'tcwv_full': {'dtype': 'int16', 'scale_factor': 0.01, '_FillValue': -9999, "zlib": True, # "complevel": complevel}, 'tcwv': {'dtype': 'int16', 'scale_factor': 0.01, '_FillValue': -9999, "zlib": True, "complevel": complevel}, 'tcwv_std': {'dtype': 'int16', 'scale_factor': 0.01, '_FillValue': -9999, "zlib": True, "complevel": complevel}} # clean up before exporting netcdf output if os.path.exists(ofile): os.remove(ofile) odir = os.path.dirname(ofile) if not os.path.exists(odir): os.mkdir(odir) self.l2_prod.sel(wl=slice(400, 1150)).to_netcdf(ofile, encoding=encoding) # l2_prod.close() return