diff --git a/doc/sphinx/source/input.rst b/doc/sphinx/source/input.rst index 23b791149c..906dd2af27 100644 --- a/doc/sphinx/source/input.rst +++ b/doc/sphinx/source/input.rst @@ -424,6 +424,8 @@ A list of the datasets for which a CMORizers is available is provided in the fol +----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+ | MLS-AURA* [#t3]_ | hur, hurStderr (day) | 3 | Python | +----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+ +| MISR | od550aer, abs550aer (AERmon) | 3 | Python | ++----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+ | MOBO-DIC-MPIM | dissic (Omon) | 2 | Python | +----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+ | MOBO-DIC2004-2019 | dissic (Omon) | 2 | Python | diff --git a/esmvaltool/cmorizers/data/cmor_config/MISR.yml b/esmvaltool/cmorizers/data/cmor_config/MISR.yml new file mode 100644 index 0000000000..351cfbfede --- /dev/null +++ b/esmvaltool/cmorizers/data/cmor_config/MISR.yml @@ -0,0 +1,19 @@ +# Global attributes of NetCDF file +attributes: + project_id: OBS6 + dataset_id: MISR + modeling_realm: atmos + type: sat + tier: 3 + version: 'V0032' + source: 'https://asdc.larc.nasa.gov/project/MISR/MIL3MAEN_004' + reference: 'misr' + files: 'MISR_AM1_CGAS_*_F15_0032.nc' +# Variables to CMORize +variables: + od550aer: + raw_name: Aerosol_Optical_Depth + mip: AERmon + abs550aer: + raw_name: Absorbing_Optical_Depth + mip: AERmon diff --git a/esmvaltool/cmorizers/data/datasets.yml b/esmvaltool/cmorizers/data/datasets.yml index 5997f13864..4d4852d8a6 100644 --- a/esmvaltool/cmorizers/data/datasets.yml +++ b/esmvaltool/cmorizers/data/datasets.yml @@ -1082,6 +1082,16 @@ datasets: info: | Download the file MPI_MOBO-DIC_2004-2019_v2.nc + MISR: + tier: 3 + source: https://asdc.larc.nasa.gov/project/MISR/MIL3MAEN_004 + last_access: 2026-02-17 + info: | + To obtain the data set you will need to go to the Data Distribution + section of the link, create an account/sign in on the earthdata.nasa.gov website + and then follow the instructions for downloading the data. + The data is currently freely available, but a registration is required. + MODIS: tier: 3 source: https://ladsweb.modaps.eosdis.nasa.gov/search/order diff --git a/esmvaltool/cmorizers/data/formatters/datasets/misr.py b/esmvaltool/cmorizers/data/formatters/datasets/misr.py new file mode 100644 index 0000000000..c72a37d3fb --- /dev/null +++ b/esmvaltool/cmorizers/data/formatters/datasets/misr.py @@ -0,0 +1,166 @@ +"""ESMValTool CMORizer for MISR data. + +Tier + Tier 3 + +Source + ftp://l5ftl01.larc.nasa.gov/MISR/MIL3MAEN.004/ +""" + +import copy +import datetime as dt +import logging +import os +from pathlib import Path + +import numpy as np +import xarray as xr +from dask import array as da +from esmvalcore.cmor.table import CMOR_TABLES + +from esmvaltool.cmorizers.data import utilities as utils + +logger = logging.getLogger(__name__) + +band550 = {"name": "green_558nm", "lambda": 558} + + +def _extract_variable(short_name, var, cfg, in_dir, out_dir): + attrs = copy.deepcopy(cfg["attributes"]) + attrs["mip"] = var["mip"] + ver = attrs["version"] + files = attrs["files"] + raw_var = var.get("raw_name", short_name) + + cmor_table = CMOR_TABLES[attrs["project_id"]] + cmor_info = cmor_table.get_variable(var["mip"], short_name) + + """Extract variable.""" + # load data + logger.debug( + "Loading data from file(s) '%s' in directory '%s' with version '%s'", + files, + in_dir, + ver, + ) + for filepath in Path(os.path.join(in_dir)).glob(files): + xrds = xr.open_dataset(filepath, group="Aerosol_Parameter_Average") + xrvar = xrds.sel(Band=band550["name"], Optical_Depth_Range="all")[ + raw_var + ] + + # change order of latitude and longitude coordinates + # xrvar = xrvar.transpose() + + # Add additional coordinates before converting to an iris cube, as this is easier with xarray + + # Time not present in source data, needs to be added manually + # Determine time from filename: + fileparts = str(filepath).split("_") + year = int(fileparts[-3]) + monthstr = fileparts[-4] + month = [ + "JAN", + "FEB", + "MAR", + "APR", + "MAY", + "JUN", + "JUL", + "AUG", + "SEP", + "OCT", + "NOV", + "DEC", + ].index(monthstr) + 1 + days_since_1850 = dt.date(year, month, 15) - dt.date(1850, 1, 1) + lb_since_1850 = dt.date(year, month, 1) - dt.date(1850, 1, 1) + if month == 12: + ub_since_1850 = dt.date(year + 1, 1, 1) - dt.date(1850, 1, 1) + else: + ub_since_1850 = dt.date(year, month + 1, 1) - dt.date(1850, 1, 1) + + xrvar = xrvar.assign_coords(time=days_since_1850.days) + xrvar = xrvar.expand_dims("time", axis=2) + xrvar["time"].attrs["units"] = "days since 1850-01-01 00:00:00" + + if short_name in ["od550aer", "abs550aer"]: + xrvar = xrvar.assign_coords(radiation_wavelength=band550["lambda"]) + xrvar["radiation_wavelength"].attrs["units"] = "nm" + + cube = xrvar.to_iris() + + # Fix metadata + cube.coord("Geodetic Latitude").rename("latitude") + cube.coord("Geodetic Longitude").rename("longitude") + + # add time bounds + cube.coord("time").bounds = np.array( + [ub_since_1850.days, lb_since_1850.days] + ) + + utils.fix_var_metadata(cube, cmor_info) + utils.set_global_atts(cube, attrs) + + utils.fix_dim_coordnames(cube) + + # When Dask tries to roll this cube, it fails because it can't chunk this properly + # So here we replicate the part of fix_coords that does that, except with numpy.roll + # instead of dask.roll. + # However, it seems that those last two lines had no effect on the results... + # cube.data has shape (360, 720, 1) i.e. (lat, lon, time) + # so the original code tried to roll the cube on the time axis + # I could hard-code the lon axis (like they did), but instead I try to autodetect it. + cube_coord = cube.coord("longitude") + logger.debug("Fixing longitude...") + if cube_coord.ndim == 1: + if cube_coord.points[0] < 0.0 and cube_coord.points[-1] < 181.0: + cube_coord.points = cube_coord.points + 180.0 + cube.attributes["geospatial_lon_min"] = 0.0 + cube.attributes["geospatial_lon_max"] = 360.0 + nlon = len(cube_coord.points) + axis = cube.coords().index(cube_coord) + shift = nlon // 2 + cube.data = da.roll(cube.core_data(), shift, axis=axis) + + if np.diff(cube.coord("latitude").points)[0] < 0: + # convert [90,-90] to [-90,90] + cube.coord("latitude").points = cube.coord("latitude").points[::-1] + # flip the data + cube.data = cube.data[:, ::-1, :] # latitude is axis=1 + + lat_bounds = [] + for lat in cube.coord("latitude").points: + lat_bounds.append([lat - 0.25, lat + 0.25]) + lat_bounds = np.array(lat_bounds) + cube.coord("latitude").bounds = lat_bounds.reshape(-1, 2) + + lon_bounds = [] + for lon in cube.coord("longitude").points: + lon_bounds.append([lon - 0.25, lon + 0.25]) + lon_bounds = np.array(lon_bounds) + cube.coord("longitude").bounds = lon_bounds.reshape(-1, 2) + + utils.fix_coords(cube) + + # fix the wavelength coordinate information. + if short_name in ["od550aer", "abs550aer"]: + cube.coord("radiation_wavelength").var_name = "wavelength" + cube.coord("wavelength").standard_name = "radiation_wavelength" + + utils.set_global_atts(cube, attrs) + + # Save variable + logger.debug(f"Saving Cube: {cube}, in directory: {out_dir}") + utils.save_variable( + cube, short_name, out_dir, attrs, unlimited_dimensions=["time"] + ) + + +def cmorization(in_dir, out_dir, cfg, cfg_user, start_date, end_date): + """Run CMORizer for MISR.""" + cfg.pop("cmor_table") + + for short_name, var in cfg["variables"].items(): + logger.info(f"CMORizing variable '{short_name}'") + _extract_variable(short_name, var, cfg, in_dir, out_dir) diff --git a/esmvaltool/config-references.yml b/esmvaltool/config-references.yml index 939696123f..f0aed75a6d 100644 --- a/esmvaltool/config-references.yml +++ b/esmvaltool/config-references.yml @@ -229,6 +229,10 @@ authors: name: Winterstein, Franziska institute: DLR, Germany orcid: https://orcid.org/0000-0002-2406-4936 + fruttarol_noah: + name: Fruttarol, Noah + institute: CCCma, ECCC, Canada + github: noahfruttarol fuckar_neven: name: Fuckar, Neven institute: BSC, Spain diff --git a/esmvaltool/recipes/examples/recipe_check_obs.yml b/esmvaltool/recipes/examples/recipe_check_obs.yml index 11ee979cc6..0181320c7b 100644 --- a/esmvaltool/recipes/examples/recipe_check_obs.yml +++ b/esmvaltool/recipes/examples/recipe_check_obs.yml @@ -800,6 +800,19 @@ diagnostics: scripts: null + MISR: + description: MISR check + variables: + od550aer: + mip: AERmon + abs550aer: + mip: AERmon + additional_datasets: + - {dataset: MISR, project: OBS, mip: AERmon, tier: 3, + type: sat, version: v0032, start_year: 2000, end_year: 2020} + scripts: null + + MOBO-DIC-MPIM: description: MOBO-DIC-MPIM check variables: diff --git a/esmvaltool/references/misr.bibtex b/esmvaltool/references/misr.bibtex new file mode 100644 index 0000000000..577d498694 --- /dev/null +++ b/esmvaltool/references/misr.bibtex @@ -0,0 +1,11 @@ +@misc{misr, + doi = {10.5067/Terra/MISR/MIL3MAEN_L3.004}, + url = {https://doi.org/10.5067/Terra/MISR/MIL3MAEN_L3.004}, + publisher = {NASA Langley Atmospheric Science Data Center DAAC}, + title = {MISR Level 3 Component Global Aerosol product in netCDF format covering a month V004}, + author = {NASA/LARC/SD/ASDC}, + date = {2008-09-26}, + year = 2008, + month = 9, + day = 26, +}