Source code for eradiate.test_tools.test_cases.rami4atm

import functools

import attr
import numpy as np
import xarray as xr

from ..regression import RMSETest, ZTest
from ...experiments import CanopyAtmosphereExperiment
from ...units import unit_registry as ureg


[docs] def create_rami4atm_toa( case: str, spp: int, padding: int = 5 ) -> tuple[str, list[CanopyAtmosphereExperiment]]: """ Create an :class:`.Experiment` to simulate one of the RAMI4ATM benchmarking cases. Parameters ---------- case : str Identifier of the test case (see notes for supported values). spp : int Default measure sample count. Returns ------- tuple A tuple containing the srf identifier and a list of :class:`.Experiment` objects. Raises ------ ValueError If the passed case ID contains unhandled parameter values. Notes ----- Case IDs are structured as follows (see also the `RAMI4ATM case naming convention reference\ <https://rami-benchmark.jrc.ec.europa.eu/_www/format_RAMI4ATM.php?strPhase=RAMI4ATM>`__:: <canopy_id>_<surface_id>_<band_id>_<illumination_id>_<measure_id> .. list-table:: :widths: auto * - Parameter - Accepted values * - ``canopy_id`` - ``hom00`` (none), ``hom45`` (uniform cloud leaf) * - ``surface_id`` - ``bla`` (black), ``whi`` (white), ``lam`` (Lambertian), ``rpv`` (RPV), ``rli`` (Ross-Li) * - ``atmosphere_id`` - ``MATP`` with * ``M`` in [``0`` (no molecular component), ``a`` (molecular absorption only), ``s`` (molecular scattering only) or ``e`` (molecular absorption and scattering)] * ``A`` in [``0`` (no aerosols), ``d`` (desert aerosols) or ``c`` (continental aerosols)] * ``T`` in [``0`` (no aerosols), ``2`` (AOT = 0.2) or ``6`` (AOT = 0.6)] * ``P`` in [``s`` (U.S. Standard atmospheric profile)] * - ``band_id`` - ``m04`` (Sentinel-2 / MSI, band 4) * - ``illumination_id`` - ``z30a000`` (30° zenith, 0° azimuth) * - ``measure_id`` - ``brfpp`` (TOA BRF in the principal plane) """ canopy_id, surf_id, atm_id, band_id, illumination_id, measure_id = case.split("_") # Canopy setup if canopy_id == "hom00": canopy = None target = [0.0, 0.0, 0.0] elif canopy_id == "hom45": canopy = { "type": "discrete_canopy", "construct": "homogeneous", "lai": 3.0, "leaf_radius": 0.05 * ureg.m, "l_horizontal": 5.0 * ureg.m, "l_vertical": 2.0 * ureg.m, "nu": 1.0, "mu": 1.0, "leaf_reflectance": 0.05653, "leaf_transmittance": 0.01692, "padding": padding, } target = { "type": "rectangle", "xmin": -2.5 * ureg.m, "xmax": 2.5 * ureg.m, "ymin": -2.5 * ureg.m, "ymax": 2.5 * ureg.m, "z": 2.0 * ureg.m, } else: raise ValueError(f"Unhandled canopy id '{canopy_id}'") # Surface setup if surf_id == "bla": surface = { "type": "lambertian", "reflectance": {"type": "uniform", "value": 0.0}, } elif surf_id == "whi": surface = { "type": "lambertian", "reflectance": {"type": "uniform", "value": 1.0}, } elif surf_id == "lam": surface = { "type": "lambertian", "reflectance": {"type": "uniform", "value": 0.02806}, } elif surf_id == "rpv": surface = { "type": "rpv", "rho_0": {"type": "uniform", "value": 0.017051}, "k": {"type": "uniform", "value": 0.95}, "g": {"type": "uniform", "value": -0.1}, "rho_c": {"type": "uniform", "value": 0.017051}, } elif surf_id == "rli": surface = { "type": "rtls", "f_iso": {"type": "uniform", "value": 0.032171}, "f_vol": {"type": "uniform", "value": -0.002886}, "f_geo": {"type": "uniform", "value": 0.001949}, } else: raise ValueError(f"Unhandled surface id '{surf_id}'") # Atmosphere setup component_id, aerosol_id, aerosol_ot = atm_id[:3] if component_id == "0": molecular_atmosphere = None elif component_id in {"a", "s", "e"}: molecular_atmosphere = { "type": "molecular", "thermoprops": { "identifier": "afgl_1986-us_standard", "z": np.arange(0, 120.05, 0.05) * ureg.km, }, "absorption_data": "monotropa", } if component_id == "a": molecular_atmosphere.update( {"has_absorption": True, "has_scattering": False} ) elif component_id == "s": molecular_atmosphere.update( {"has_absorption": False, "has_scattering": True} ) else: # component_id == "e": molecular_atmosphere.update( {"has_absorption": True, "has_scattering": True} ) else: raise ValueError( f"Unhandled molecular atmosphere component id '{component_id}'" ) if aerosol_id == "0": particle_layer = {} elif aerosol_id in {"c", "d"}: particle_layer = { "bottom": 0.0 * ureg.m, "top": 2000.0 * ureg.m, "distribution": {"type": "uniform"}, } if aerosol_id == "c": particle_layer["dataset"] = "govaerts_2021-continental" else: # aerosol_id == "d": particle_layer["dataset"] = "govaerts_2021-desert" else: raise ValueError(f"Unhandled particle layer component id '{aerosol_id}'") if aerosol_ot == "0": pass elif aerosol_ot == "2": particle_layer["tau_ref"] = 0.2 elif aerosol_ot == "6": particle_layer["tau_ref"] = 0.6 else: raise ValueError(f"Unhandled particle layer optical thickness '{aerosol_ot}'") atmosphere = { "type": "heterogeneous", "molecular_atmosphere": molecular_atmosphere, "particle_layers": [particle_layer] if particle_layer else [], } srf_id = { "m02": "sentinel_2a-msi-2", "m03": "sentinel_2a-msi-3", "m04": "sentinel_2a-msi-4", "m8a": "sentinel_2a-msi-8a", "m11": "sentinel_2a-msi-11", "m12": "sentinel_2a-msi-12", }[band_id] # Final configuration config = { "canopy": canopy, "surface": surface, "atmosphere": atmosphere, "illumination": { "type": "directional", "zenith": 30.0 * ureg.deg, "azimuth": 0.0 * ureg.deg, }, "measures": [ { "type": "mdistant", "construct": "hplane", "zeniths": np.arange(-75, 76, 2) * ureg.deg, "azimuth": 0.0 * ureg.deg, "srf": srf_id, "spp": spp, "target": target, } ], "ckd_quad_config": {"type": "gauss_legendre", "ng_max": 16, "policy": "fixed"}, "integrator": {"type": "piecewise_volpath", "moment": True}, } return srf_id, [CanopyAtmosphereExperiment(**config)]
[docs] def create_rami4atm_boa( toa_case: str, spp: int ) -> tuple[str, list[CanopyAtmosphereExperiment]]: """ Create BOA experiments from Rami4ATM case id Return 4 experiments for each BOA case, computing observed the upwelling radiance and radiosity for the target, and ones that would have been reflected by a perfectly diffuse surface. """ ctor = registry[toa_case]["constructor"] srf_id, (exp0,) = ctor(spp=spp) if "hom00" in toa_case: extra_objects = { "boa_white_reference_patch": { "factory": "shape", "type": "rectangle", "center": [0, 0, 0.01], "edges": [1, 1], "bsdf": {"type": "lambertian", "reflectance": 1.0}, } } target = [0, 0, 0.01] else: extra_objects = { "boa_white_reference_patch": { "factory": "shape", "type": "rectangle", "center": [0, 0, 2.025], "edges": [5, 5], "bsdf": {"type": "lambertian", "reflectance": 1.0}, } } target = { "type": "rectangle", "xmin": -2.5, "xmax": 2.5, "ymin": -2.5, "ymax": 2.5, "z": 2.025, } exp1 = attr.evolve( exp0, measures=[ { "type": "mdistant", "spp": spp, "ray_offset": 0.05, "srf": srf_id, "construct": "hplane", "zeniths": np.arange(-75, 76, 1), "zeniths_units": "degree", "azimuth": 0.0, "azimuth_units": "degree", "target": target, } ], ) exp2 = attr.evolve(exp1, extra_objects=extra_objects) dflux = { "type": "distantflux", "ray_offset": 0.05, "target": target, "srf": srf_id, "spp": spp, } exp3 = attr.evolve(exp1, measures=[dflux]) exp4 = attr.evolve(exp2, measures=[dflux]) return srf_id, [exp1, exp2, exp3, exp4]
def _calculate_boa_reflectance_factor_var( da1: xr.Dataset, var1: xr.Dataset, da2: xr.Dataset, var2: xr.Dataset, srf: xr.Dataset, ) -> xr.Dataset: # Handle numerical variance estimates, assuming: # - Gaussian nature # - Null covariance of different experiments or pixels # - Local linearity of radiance and radiosity # - Scalar SRF srf_weight = srf.srf.interp(w=da1.w.values).fillna(1e-18) srf_sum = srf_weight.sum() S1 = da1.sum() / srf_sum S2 = da2.sum() / srf_sum v_S1 = (var1 * srf_weight**2).sum(dim="w") / (srf_weight**2).sum() v_S2 = (var2 * srf_weight**2).sum(dim="w") / (srf_weight**2).sum() d_R_d_S1 = 1 / S2 d_R_d_S2 = -S1 / S2**2 v_R = d_R_d_S1**2 * v_S1 + d_R_d_S2**2 * v_S2 return v_R def postprocess_boa_cases(results: list[xr.Dataset], srf: xr.Dataset) -> xr.Dataset: result1, result2, result3, result4 = results result_hdrf = result1.radiance_srf / result2.radiance_srf result_bhr = result3.radiosity_srf / result4.radiosity_srf # Combine a reference dataset result = result1.rename( {"radiance_srf": "radiance_srf1", "radiance_var": "radiance_var1"} ) result["radiance_srf2"] = result2.radiance_srf result["radiance_var2"] = result2.radiance_var result["radiosity_srf3"] = result3.radiosity_srf result["radiosity_var3"] = result3.sector_radiosity_var result["radiosity_srf4"] = result4.radiosity_srf result["radiosity_var4"] = result4.sector_radiosity_var result["hdrf"] = result_hdrf result["bhr"] = result_bhr result["hdrf_var"] = _calculate_boa_reflectance_factor_var( result1.radiance, result1.radiance_var, result2.radiance, result2.radiance_var, srf, ) result["bhr_var"] = _calculate_boa_reflectance_factor_var( result3.radiosity, result3.sector_radiosity_var, result4.radiosity, result4.sector_radiosity_var, srf, ) return result #: Mapping of test case to a generation function for all RAMI4ATM test cases #: available in the Eradiate regression test suite. registry = { case: { "constructor": functools.partial(create_rami4atm_toa, case=case), "threshold": 0.005, } for case in [ "hom00_whi_s00s_m04_z30a000_brfpp", "hom00_bla_a00s_m04_z30a000_brfpp", "hom00_rpv_e00s_m04_z30a000_brfpp", "hom00_rpv_0c2s_m04_z30a000_brfpp", "hom00_rpv_0c6s_m04_z30a000_brfpp", "hom00_rpv_0d2s_m04_z30a000_brfpp", "hom00_rpv_0d6s_m04_z30a000_brfpp", "hom00_rpv_sc2s_m04_z30a000_brfpp", "hom00_rpv_sc6s_m04_z30a000_brfpp", "hom00_rpv_sd2s_m04_z30a000_brfpp", "hom00_rpv_sd6s_m04_z30a000_brfpp", "hom00_rpv_ac2s_m04_z30a000_brfpp", "hom00_rpv_ac6s_m04_z30a000_brfpp", "hom00_rpv_ad2s_m04_z30a000_brfpp", "hom00_rpv_ad6s_m04_z30a000_brfpp", "hom00_lam_ec2s_m04_z30a000_brfpp", "hom00_rpv_ec2s_m04_z30a000_brfpp", "hom00_rli_ec2s_m04_z30a000_brfpp", "hom00_rpv_ec6s_m04_z30a000_brfpp", "hom00_rpv_ed2s_m04_z30a000_brfpp", "hom00_rpv_ed6s_m04_z30a000_brfpp", "hom45_lam_ec2s_m04_z30a000_brfpp", ] } registry["hom00_bla_sd2s_m03_z30a000_brfpp"] = { "constructor": functools.partial( create_rami4atm_toa, case="hom00_bla_sd2s_m03_z30a000_brfpp" ), "test": ZTest, "threshold": 0.05, "variables": ["radiance"], } registry["hom00_whi_s00s_m04_z30a000_boa"] = { "constructor": functools.partial( create_rami4atm_boa, toa_case="hom00_whi_s00s_m04_z30a000_brfpp" ), "test": RMSETest, "threshold": 2e-3, "variables": ["hdrf", "bhr"], "postprocess": postprocess_boa_cases, } registry["hom00_rpv_e00s_m04_z30a000_boa"] = { "constructor": functools.partial( create_rami4atm_boa, toa_case="hom00_rpv_e00s_m04_z30a000_brfpp" ), "test": RMSETest, "threshold": 2e-5, "variables": ["hdrf", "bhr"], "postprocess": postprocess_boa_cases, }