Skip to content

Commit 479e0bd

Browse files
axel-lauerschlunmajlenh
authored
CMORizer for ESACCI-BIOMASS (#4121)
Co-authored-by: Manuel Schlund <32543114+schlunma@users.noreply.github.com> Co-authored-by: Julien Lenhardt <45034763+jlenh@users.noreply.github.com>
1 parent 14b348e commit 479e0bd

7 files changed

Lines changed: 284 additions & 1 deletion

File tree

doc/sphinx/source/input.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,10 @@ A list of the datasets for which a CMORizers is available is provided in the fol
311311
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
312312
| ESACCI-AEROSOL | abs550aer, od550aer, od550aerStderr, od550lt1aer, od870aer, od870aerStderr (aero) | 2 | NCL |
313313
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
314+
| ESACCI-BIOMASS | agb (Lyr, frequency=yr) | 2 | Python |
315+
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
314316
| ESACCI-CLOUD | clivi, clt, cltStderr, clwvi, lwp, rlut, rlutcs, rsut, rsutcs, rsdt, rlus, rsus, rsuscs (Amon), | 2 | Python |
315-
| | clt, clwvi, cod (day) | 2 | |
317+
| | clt, clwvi, cod (day) | | |
316318
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
317319
| ESACCI-FIRE | burntArea (Lmon) | 2 | NCL |
318320
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
attributes:
2+
project_id: 'OBS6'
3+
dataset_id: 'ESACCI-BIOMASS'
4+
tier: 2
5+
modeling_realm: sat
6+
institution: 'GAMMA Remote Sensing'
7+
reference: 'esacci-biomass'
8+
source: 'ftp://anon-ftp.ceda.ac.uk/neodc/esacci/biomass/data/agb/maps/'
9+
title: 'ESA CCI Biomass'
10+
version: 'v6.0'
11+
comment: ''
12+
variables:
13+
agb:
14+
mip: Lyr
15+
frequency: yr
16+
long_name: 'Above-ground biomass'
17+
raw: agb
18+
filename: ESACCI-BIOMASS-L4-AGB-MERGED-10000m-fv6.0.nc

esmvaltool/cmorizers/data/datasets.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,19 @@ datasets:
451451
Other years are not considered since they are not complete.
452452
Put all files in input_dir_path (no subdirectories with years).
453453
454+
ESACCI-BIOMASS:
455+
tier: 2
456+
source: ftp://anon-ftp.ceda.ac.uk/neodc/esacci/biomass/data/agb/maps/v6.0/netcdf
457+
last_access: 2025-07-16
458+
info: |
459+
Download and processing instructions:
460+
Use the following CLI to download all the files:
461+
esmvaltool data download ESACCI-BIOMASS
462+
The underlying downloader is located here:
463+
/ESMValTool/esmvaltool/cmorizers/data/downloaders/datasets/esacci_biomass.py
464+
and it will download all the file regridded to 10 km available on CEDA (2007, 2010, 2015-2021)
465+
under a single directory as follow: ${RAWOBS}/Tier2/ESACCI-BIOMASS
466+
454467
ESACCI-CLOUD:
455468
tier: 2
456469
source: https://public.satproj.klima.dwd.de/data/ESA_Cloud_CCI/CLD_PRODUCTS/v3.0/
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Script to download ESACCI-BIOMASS agb data from the CEDA."""
2+
3+
from esmvaltool.cmorizers.data.downloaders.ftp import CCIDownloader
4+
5+
6+
def download_dataset(
7+
config,
8+
dataset,
9+
dataset_info,
10+
start_date,
11+
end_date,
12+
overwrite,
13+
):
14+
"""Download dataset.
15+
16+
Parameters
17+
----------
18+
config : dict
19+
ESMValTool's user configuration
20+
dataset : str
21+
Name of the dataset
22+
dataset_info : dict
23+
Dataset information from the datasets.yml file
24+
start_date : datetime
25+
Start of the interval to download
26+
end_date : datetime
27+
End of the interval to download
28+
overwrite : bool
29+
Overwrite already downloaded files
30+
"""
31+
# Initialize the downloader
32+
downloader = CCIDownloader(
33+
config=config,
34+
dataset=dataset,
35+
dataset_info=dataset_info,
36+
overwrite=overwrite,
37+
)
38+
downloader.ftp_name = "biomass"
39+
downloader.connect()
40+
41+
# Set current working directory to the main directory with the files
42+
downloader.set_cwd("/agb/maps/v6.0/netcdf")
43+
44+
# Download 10 km file
45+
downloader.download_file("ESACCI-BIOMASS-L4-AGB-MERGED-10000m-fv6.0.nc")
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""ESMValTool CMORizer for ESACCI-BIOMASS above-ground biomass (agb) data.
2+
3+
Tier
4+
Tier 2: other freely-available dataset.
5+
6+
Source
7+
ftp://anon-ftp.ceda.ac.uk/neodc/esacci/biomass/data/agb/maps
8+
9+
Last access
10+
20250716
11+
12+
Download and processing instructions
13+
Download 10 km file:
14+
v6.0/netcd/ESACCI-BIOMASS-L4-AGB-MERGED-10000m-fv6.0.nc
15+
Put file in ${RAWOBS}/Tier2/ESACCI-BIOMASS
16+
Or use automatic download script:
17+
esmvaltool data download ESACCI-BIOMASS
18+
"""
19+
20+
import datetime
21+
import glob
22+
import logging
23+
import os
24+
from copy import deepcopy
25+
26+
import iris
27+
import numpy as np
28+
from dask import array as da
29+
from esmvalcore.cmor.table import CMOR_TABLES
30+
from esmvalcore.preprocessor import extract_time
31+
32+
from esmvaltool.cmorizers.data.utilities import (
33+
flip_dim_coord,
34+
save_variable,
35+
set_global_atts,
36+
)
37+
38+
# Configure logging
39+
logger = logging.getLogger(__name__)
40+
41+
42+
def _extract_variable(in_files, var, cfg, out_dir):
43+
logger.info(
44+
"CMORizing variable '%s' from input files '%s'",
45+
var["short_name"],
46+
", ".join(in_files),
47+
)
48+
attributes = deepcopy(cfg["attributes"])
49+
attributes["mip"] = var["mip"]
50+
attributes["raw"] = var["raw"]
51+
attributes["frequency"] = var["frequency"]
52+
cmor_table = CMOR_TABLES[attributes["project_id"]]
53+
definition = cmor_table.get_variable(var["mip"], var["short_name"])
54+
55+
# load all input files (1 year) into 1 cube
56+
cube_list = iris.load(in_files, var["raw"])
57+
58+
drop_attrs = ["valid_max", "valid_min"]
59+
60+
for cube in cube_list:
61+
# set correct names
62+
cube.var_name = definition.short_name
63+
cube.standard_name = definition.standard_name
64+
cube.long_name = definition.long_name
65+
66+
for attr in drop_attrs:
67+
if attr in cube.attributes:
68+
cube.attributes.pop(attr)
69+
70+
cube.coord("time").points = (
71+
cube.coord("time").core_points().astype("float64")
72+
)
73+
74+
# fix units
75+
cube.convert_units(definition.units)
76+
77+
# set global attributes
78+
set_global_atts(cube, attributes)
79+
80+
# roll longitude (-180...180 --> 0...360)
81+
cube.coord("longitude").points = cube.coord("longitude").points + 180.0
82+
nlon = len(cube.coord("longitude").points)
83+
cube.data = da.roll(cube.core_data(), int(nlon / 2), axis=2)
84+
85+
# remove rouding errors introduced by da.roll
86+
loncoord = cube.coord("longitude")
87+
latcoord = cube.coord("latitude")
88+
loncoord.points = np.round(loncoord.core_points(), 3)
89+
latcoord.points = np.round(latcoord.core_points(), 3)
90+
91+
# flip latitudes
92+
flip_dim_coord(cube, "latitude")
93+
94+
# fix coordinates
95+
cube = _fix_coordinates(cube, definition)
96+
cube.coord("latitude").attributes = None
97+
cube.coord("longitude").attributes = None
98+
99+
# save each year to a separate output file
100+
timecoord = cube.coord("time")
101+
for time in timecoord.units.num2date(timecoord.points):
102+
# extract current year
103+
outcube = extract_time(cube, time.year, 1, 1, time.year, 12, 31)
104+
# adjust time bounds to (year-01-01 00:00, year+1-01-01 00:00)
105+
out_timecoord = outcube.coord("time")
106+
start_date = datetime.datetime(time.year, 1, 1)
107+
end_date = datetime.datetime(time.year + 1, 1, 1)
108+
out_timecoord.bounds = np.array(
109+
[
110+
out_timecoord.units.date2num(start_date),
111+
out_timecoord.units.date2num(end_date),
112+
]
113+
)
114+
# write output to file
115+
logger.debug("Saving cube\n%s", outcube)
116+
logger.debug("Setting time dimension to UNLIMITED while saving!")
117+
save_variable(
118+
outcube,
119+
var["short_name"],
120+
out_dir,
121+
attributes,
122+
unlimited_dimensions=["time"],
123+
)
124+
125+
logger.info("Finished CMORizing %s", ", ".join(in_files))
126+
127+
128+
def _fix_coordinates(cube, definition):
129+
"""Fix coordinates."""
130+
axis2def = {"T": "time", "X": "longitude", "Y": "latitude"}
131+
axes = ["T", "X", "Y"]
132+
133+
for axis in axes:
134+
coord_def = definition.coordinates.get(axis2def[axis])
135+
if coord_def:
136+
coord = cube.coord(axis=axis)
137+
if axis == "T":
138+
coord.convert_units("days since 1850-1-1 00:00:00.0")
139+
coord.points = coord.core_points().astype("float64")
140+
if coord.bounds is not None:
141+
coord.bounds = None
142+
143+
if len(coord.points) > 1:
144+
if coord.bounds is not None:
145+
coord.bounds = None
146+
coord.guess_bounds()
147+
coord.standard_name = coord_def.standard_name
148+
coord.var_name = coord_def.out_name
149+
coord.long_name = coord_def.long_name
150+
151+
return cube
152+
153+
154+
def cmorization(in_dir, out_dir, cfg, cfg_user, start_date, end_date):
155+
"""Cmorize data."""
156+
glob_attrs = cfg["attributes"]
157+
158+
logger.info(
159+
"Starting cmorization for tier%s OBS files: %s",
160+
glob_attrs["tier"],
161+
glob_attrs["dataset_id"],
162+
)
163+
logger.info("Input data from: %s", in_dir)
164+
logger.info("Output will be written to: %s", out_dir)
165+
logger.info("CMORizing ESACCI-BIOMASS version %s", glob_attrs["version"])
166+
167+
for short_name, var in cfg["variables"].items():
168+
filepattern = os.path.join(in_dir, var["filename"])
169+
in_files = glob.glob(filepattern)
170+
if "short_name" not in var:
171+
var["short_name"] = short_name
172+
if not in_files:
173+
msg = f"no data not found for variable {short_name}"
174+
raise ValueError(msg)
175+
_extract_variable(in_files, var, cfg, out_dir)

esmvaltool/recipes/examples/recipe_check_obs.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,26 @@ diagnostics:
202202
scripts: null
203203

204204

205+
ESACCI-BIOMASS:
206+
description: ESACCI-BIOMASS check
207+
variables:
208+
agb_2007:
209+
start_year: 2007
210+
end_year: 2007
211+
short_name: agb
212+
agb_2010:
213+
start_year: 2010
214+
end_year: 2010
215+
short_name: agb
216+
agb_2015-2022:
217+
start_year: 2015
218+
end_year: 2022
219+
short_name: agb
220+
additional_datasets:
221+
- {dataset: ESACCI-BIOMASS, project: OBS6, mip: Lyr, tier: 2, type: sat, version: v6.0, frequency: yr}
222+
scripts: null
223+
224+
205225
ESACCI-CLOUD:
206226
description: ESACCI-CLOUD check
207227
variables:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@misc{esacci-biomass,
2+
doi = {10.5285/95913ffb6467447ca72c4e9d8cf30501},
3+
url = {https://dx.doi.org/10.5285/95913ffb6467447ca72c4e9d8cf30501},
4+
year = {2025},
5+
month = {apr},
6+
day = {17},
7+
publisher = {NERC EDS Centre for Environmental Data Analysis},
8+
author = {Santoro, M. and Cartus, O.},
9+
title = {ESA Biomass Climate Change Initiative (Biomass{\_}cci): Global datasets of forest above-ground biomass for the years 2007, 2010, 2015, 2016, 2017, 2018, 2019, 2020, 2021 and 2022, v6.0},
10+
}

0 commit comments

Comments
 (0)