Skip to content

Commit 935888a

Browse files
schlunmajlenhvaleriupredoi
authored
Added CMORizer for Yang2020 data (#4090)
Co-authored-by: Julien Lenhardt <45034763+jlenh@users.noreply.github.com> Co-authored-by: Valeriu Predoi <valeriu.predoi@gmail.com>
1 parent 40f9b22 commit 935888a

8 files changed

Lines changed: 285 additions & 20 deletions

File tree

doc/sphinx/source/input.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,9 @@ A list of the datasets for which a CMORizers is available is provided in the fol
489489
| WOA | thetao, so, tos, sos (Omon) | 2 | Python |
490490
| | no3, o2, po4, si (Oyr) | | |
491491
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
492+
| Yang2020 | dpn2o, no2flux (Omon) | 2 | Python |
493+
| | areacella (fx) | | |
494+
+------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
492495

493496
.. [#t3] We obtained permission from the dataset provider to share this dataset
494497
among ESMValTool users on HPC systems.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
# Common global attributes for Cmorizer output
3+
attributes:
4+
dataset_id: Yang2020
5+
version: "1"
6+
tier: 2
7+
modeling_realm: reanaly
8+
project_id: OBS6
9+
source: https://doi.org/10.26008/1912/bco-dmo.810032.1
10+
reference: yang2020
11+
12+
13+
# Variables to CMORize
14+
variables:
15+
areacella:
16+
filename: n2oFlux-Yang2020.nc
17+
mip: fx
18+
raw_name: cellArea_m2
19+
dpn2o:
20+
filename: dn2o-mapped-Yang2020.nc
21+
mip: Omon
22+
raw_name: dn2o_EnsMean_natm
23+
comment: >-
24+
Time bounds of the climatology are set to 1971-2018 since measurements
25+
that serve as input for this data product have been conducted between
26+
1971 and 2018. This has been recommended by a co-author of the original
27+
publication (https://doi.org/10.1073/pnas.1921914117) via e-mail, but is
28+
NOT explicitly stated in the original publication. Thus, the period given
29+
here needs to be treated with care.
30+
n2oflux:
31+
filename: n2oFlux-Yang2020.nc
32+
mip: Omon
33+
raw_name: n2oFlux_EnsMean_g-pm2-pyr
34+
raw_units: g m-2 yr-1
35+
molar_mass: 44.013 # [g/mol]
36+
comment: >-
37+
Time bounds of the climatology are set to 1971-2018 since measurements
38+
that serve as input for this data product have been conducted between
39+
1971 and 2018 (note that the wind products from ERA5 and CCMP, which are
40+
also used as input, are only available from 1988-2017 though). This has
41+
been recommended by a co-author of the original publication
42+
(https://doi.org/10.1073/pnas.1921914117) via e-mail, but is NOT
43+
explicitly stated in the original publication. Thus, the period given
44+
here needs to be treated with care.

esmvaltool/cmorizers/data/datasets.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,3 +1391,14 @@ datasets:
13911391
silicate/netcdf/all/1.00/woa18_all_i00_01.nc
13921392
(To get WOA13, replace filenames prefix woa18 with woa13 and source with
13931393
https://www.ncei.noaa.gov/data/oceans/woa/WOA13/DATAv2)
1394+
1395+
Yang2020:
1396+
tier: 2
1397+
source: |
1398+
https://doi.org/10.26008/1912/bco-dmo.810032.1
1399+
last_access: 2025-06-10
1400+
info: |
1401+
Use automatic download feature to get the data. This will download the
1402+
files:
1403+
dn2o-mapped-Yang2020.nc
1404+
n2oFlux-Yang2020.nc
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Download Yang2020 data."""
2+
3+
import logging
4+
5+
from esmvaltool.cmorizers.data.downloaders.wget import WGetDownloader
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def download_dataset(
11+
config, dataset, dataset_info, start_date, end_date, overwrite
12+
):
13+
"""Download dataset.
14+
15+
Parameters
16+
----------
17+
config : dict
18+
ESMValTool's user configuration
19+
dataset : str
20+
Name of the dataset
21+
dataset_info : dict
22+
Dataset information from the datasets.yml file
23+
start_date : datetime
24+
Start of the interval to download
25+
end_date : datetime
26+
End of the interval to download
27+
overwrite : bool
28+
Overwrite already downloaded files
29+
"""
30+
downloader = WGetDownloader(
31+
config=config,
32+
dataset=dataset,
33+
dataset_info=dataset_info,
34+
overwrite=overwrite,
35+
)
36+
37+
downloader.download_file(
38+
"https://darchive.mblwhoilibrary.org/bitstreams/"
39+
"c407fd4e-0fb9-52ba-93b8-5188994d02dd/download",
40+
wget_options=[],
41+
output_filename="n2oFlux-Yang2020.nc",
42+
)
43+
downloader.download_file(
44+
"https://darchive.mblwhoilibrary.org/bitstreams/"
45+
"266ae58f-b915-536e-af17-279d276e4295/download",
46+
wget_options=[],
47+
output_filename="dn2o-mapped-Yang2020.nc",
48+
)

esmvaltool/cmorizers/data/downloaders/wget.py

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""wget based downloader."""
22

33
import logging
4-
import os
54
import subprocess
5+
from pathlib import Path
66

77
from .downloader import BaseDownloader
88

@@ -30,8 +30,8 @@ def download_folder(self, server_path, wget_options):
3030
command = (
3131
["wget"]
3232
+ wget_options
33-
+ self.overwrite_options
3433
+ [
34+
"--no-clobber",
3535
f"--directory-prefix={self.local_folder}",
3636
"--recursive",
3737
"--no-directories",
@@ -41,7 +41,7 @@ def download_folder(self, server_path, wget_options):
4141
logger.debug(command)
4242
subprocess.check_output(command)
4343

44-
def download_file(self, server_path, wget_options):
44+
def download_file(self, server_path, wget_options, output_filename=None):
4545
"""Download file.
4646
4747
Parameters
@@ -50,19 +50,44 @@ def download_file(self, server_path, wget_options):
5050
Path to remote file
5151
wget_options: list(str)
5252
Extra options for wget
53+
output_filename: str, optional
54+
Name of the downloaded file. If not given, use the one given by
55+
``server_path``.
56+
5357
"""
58+
output_dir = Path(self.local_folder)
59+
output_dir.mkdir(parents=True, exist_ok=True)
60+
if output_filename is None:
61+
output_path = output_dir / Path(server_path).name
62+
else:
63+
output_path = output_dir / output_filename
64+
output_options = []
65+
66+
# If no specific output filename is desired (i.e., the option -O can be
67+
# omitted), wget can be used with the --no-clobber and
68+
# --directory-prefix options to avoid overwriting data. Otherwise, we
69+
# will need to check file existence manually here (-O and --no-clobber
70+
# do not work well together).
71+
if not self.overwrite and output_filename is None:
72+
output_options.append(f"--directory-prefix={str(output_dir)}")
73+
output_options.append("--no-clobber")
74+
else:
75+
if (
76+
not self.overwrite
77+
and output_filename is not None
78+
and output_path.exists()
79+
):
80+
logger.info("File %s exists, skipping download", output_path)
81+
return
82+
output_options.append("-O")
83+
output_options.append(str(output_path))
84+
5485
command = (
5586
["wget"]
5687
+ wget_options
57-
+ self.overwrite_options
58-
+ [
59-
f"--directory-prefix={self.local_folder}",
60-
"--no-directories",
61-
server_path,
62-
]
88+
+ output_options
89+
+ ["--no-directories", server_path]
6390
)
64-
if self.overwrite:
65-
command.append(f"-O {os.path.basename(server_path)}")
6691
logger.debug(command)
6792
subprocess.check_output(command)
6893

@@ -80,15 +105,6 @@ def login(self, server_path, wget_options):
80105
logger.debug(command)
81106
subprocess.check_output(command)
82107

83-
@property
84-
def overwrite_options(self):
85-
"""Get overwrite options as configured in downloader."""
86-
if not self.overwrite:
87-
return [
88-
"--no-clobber",
89-
]
90-
return []
91-
92108

93109
class NASADownloader(WGetDownloader):
94110
"""Downloader for the NASA repository."""
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""CMORize Yang2020 data."""
2+
3+
import logging
4+
from datetime import datetime
5+
from pathlib import Path
6+
7+
import iris
8+
from cf_units import Unit
9+
from iris import NameConstraint
10+
from iris.coords import CellMethod, DimCoord
11+
12+
from esmvaltool.cmorizers.data import utilities as utils
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
TIME_UNITS = Unit("days since 1950-01-01 00:00:00", calendar="standard")
18+
19+
20+
def _fix_climatological_time(cube):
21+
"""Fix climatology coordinate."""
22+
# Time bounds of the climatology are set to 1971-2018 since measurements
23+
# that serve as input for this data product have been conducted between
24+
# 1971 and 2018 (note that the wind products from ERA5 and CCMP, which are
25+
# necessary to derive the N2O flux, are only available from 1988-2017
26+
# though). This has been recommended by a co-author of the original
27+
# publication (https://doi.org/10.1073/pnas.1921914117) via email, but is
28+
# NOT explicitly stated in the original publication. Thus, the period given
29+
# here needs to be treated with care.
30+
time_points = TIME_UNITS.date2num(
31+
[datetime(1994, m, 15) for m in range(1, 13)]
32+
)
33+
time_bounds = [
34+
[datetime(1971, m, 1), datetime(2018, m + 1, 1)] for m in range(1, 12)
35+
]
36+
time_bounds.append([datetime(1971, 12, 1), datetime(2019, 1, 1)])
37+
time_bounds = TIME_UNITS.date2num(time_bounds)
38+
39+
# Add new time coordinate to cube
40+
time_coord = DimCoord(
41+
time_points,
42+
bounds=time_bounds,
43+
standard_name="time",
44+
long_name="time",
45+
var_name="time",
46+
units=TIME_UNITS,
47+
climatological=True,
48+
)
49+
cube.remove_coord("time")
50+
cube.add_dim_coord(time_coord, 0)
51+
52+
# Fix cell methods
53+
cube.add_cell_method(CellMethod("mean within years", coords=time_coord))
54+
cube.add_cell_method(CellMethod("mean over years", coords=time_coord))
55+
56+
57+
def _fix_var_metadata(var_info, cmor_info, cube):
58+
"""Fix variable metadata."""
59+
if "raw_units" in var_info:
60+
cube.units = var_info["raw_units"]
61+
if (
62+
"g" in str(cube.units)
63+
and "mol" in cmor_info.units
64+
and "molar_mass" in var_info
65+
):
66+
cube = cube / var_info["molar_mass"]
67+
cube.units = cube.units / "g mol-1"
68+
cube.convert_units(cmor_info.units)
69+
utils.fix_var_metadata(cube, cmor_info)
70+
return cube
71+
72+
73+
def _extract_variable(var_info, cmor_info, attrs, filepath, out_dir):
74+
"""Extract variable."""
75+
var = cmor_info.short_name
76+
raw_var = var_info.get("raw_name", var)
77+
78+
cube = iris.load_cube(filepath, NameConstraint(var_name=raw_var))
79+
80+
# Fix variable metadata
81+
cube = _fix_var_metadata(var_info, cmor_info, cube)
82+
83+
# Fix coordinates
84+
if "time" in cmor_info.coordinates:
85+
_fix_climatological_time(cube)
86+
cube = utils.fix_coords(cube, overwrite_time_bounds=False)
87+
88+
# Fix global metadata
89+
utils.set_global_atts(cube, attrs)
90+
if cmor_info.positive:
91+
cube.attributes.locals["positive"] = cmor_info.positive
92+
93+
# Save variable
94+
utils.save_variable(
95+
cube,
96+
var,
97+
out_dir,
98+
attrs,
99+
local_keys=["comment", "positive"],
100+
unlimited_dimensions=["time"],
101+
)
102+
103+
104+
def cmorization(in_dir, out_dir, cfg, cfg_user, start_date, end_date):
105+
"""Cmorization func call."""
106+
cmor_table = cfg["cmor_table"]
107+
glob_attrs = cfg["attributes"]
108+
109+
# Run the cmorization
110+
for var, var_info in cfg["variables"].items():
111+
filepath = Path(in_dir) / var_info["filename"]
112+
logger.info("CMORizing variable '%s' from file %s", var, filepath)
113+
glob_attrs["mip"] = var_info["mip"]
114+
if "comment" in var_info:
115+
glob_attrs["comment"] = var_info["comment"]
116+
cmor_info = cmor_table.get_variable(var_info["mip"], var)
117+
_extract_variable(var_info, cmor_info, glob_attrs, filepath, out_dir)

esmvaltool/recipes/examples/recipe_check_obs.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,19 @@ diagnostics:
10771077
type: clim, version: 2013v2, start_year: 2000, end_year: 2000}
10781078
scripts: null
10791079

1080+
Yang2020:
1081+
description: Yang2020 check
1082+
variables:
1083+
areacella:
1084+
mip: fx
1085+
dpn2o:
1086+
mip: Omon
1087+
n2oflux:
1088+
mip: Omon
1089+
additional_datasets:
1090+
- {project: OBS6, dataset: Yang2020, tier: 2, type: reanaly, version: 1}
1091+
scripts: null
1092+
10801093

10811094
### TIER 3 ##################################################################
10821095

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@article{yang2020,
2+
title = {Global reconstruction reduces the uncertainty of oceanic nitrous oxide emissions and reveals a vigorous seasonal cycle},
3+
volume = {117},
4+
issn = {1091-6490},
5+
doi = {10.1073/pnas.1921914117},
6+
number = {22},
7+
journal = {Proceedings of the National Academy of Sciences},
8+
publisher = {Proceedings of the National Academy of Sciences},
9+
author = {Yang, Simon and Chang, Bonnie X. and Warner, Mark J. and Weber, Thomas S. and Bourbonnais, Annie M. and Santoro, Alyson E. and Kock, Annette and Sonnerup, Rolf E. and Bullister, John L. and Wilson, Samuel T. and Bianchi, Daniele},
10+
year = {2020},
11+
month = may,
12+
pages = {11954–11960}
13+
}

0 commit comments

Comments
 (0)