Skip to content

Commit 3ed5f2d

Browse files
bettina-gierlukruh
andauthored
Add CAMS cmorizer (#3749)
Co-authored-by: Lukas <lukas@uni-bremen.de>
1 parent 9262abb commit 3ed5f2d

9 files changed

Lines changed: 590 additions & 257 deletions

File tree

doc/sphinx/source/input.rst

Lines changed: 259 additions & 252 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
# Common global attributes for Cmorizer output
3+
4+
# Input
5+
filename: 'cams73_v23r1_co2_flux_surface_mm_{year}{month}.nc'
6+
start_year: 1979
7+
end_year: 2023
8+
attributes:
9+
dataset_id: CAMS
10+
version: 'v23r1'
11+
tier: 3
12+
modeling_realm: reanaly
13+
project_id: OBS6
14+
source: "https://ads.atmosphere.copernicus.eu/cdsapp#!/dataset/cams-global-greenhouse-gas-inversion"
15+
reference: 'cams'
16+
comment: ''
17+
18+
# Variables to cmorize
19+
variables:
20+
nbp:
21+
mip: Lmon
22+
positive: down
23+
varname: 'Posterior land surface upward mass flux of carbon for the whole grid box and the whole month without fossile'
24+
fgco2:
25+
mip: Omon
26+
positive: down
27+
varname: 'Posterior ocean surface upward mass flux of carbon for the whole grid box and the whole month without fossile'
28+
areacella:
29+
mip: fx
30+
varname: area
31+
areacello:
32+
mip: Ofx
33+
varname: area
34+
sftlf:
35+
mip: fx
36+
varname: lsf
37+
sftof:
38+
mip: Ofx
39+
varname: lsf

esmvaltool/cmorizers/data/datasets.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ datasets:
1818
across Australia for each observed data variable. This accounts for spatial and temporal gaps in observations.
1919
Where possible, the gridded analysis techniques provide useful estimates in data-sparse regions
2020
such as central Australia.
21-
2221
Time coverage: Site-based data are used to provide gridded climate data at the monthly timescale for rainfall (1900+).
2322
Reference: Evans, A., Jones, D.A., Smalley, R., and Lellyett, S. 2020. An enhanced gridded rainfall analysis scheme
2423
for Australia. Bureau of Meteorology Research Report. No. 41.
@@ -33,7 +32,6 @@ datasets:
3332
last_access: 2023-11-21
3433
info: |
3534
Data from NCI project requiring an NCI account and access to GADI
36-
3735
ANUClimate 2.0 consists of gridded daily and monthly climate variables across the terrestrial landmass of Australia
3836
from at least 1970 to the present. Rainfall grids are generated from 1900 to the present. The underpinning spatial
3937
models have been developed at the Fenner School of Environment and Society of the Australian National University.
@@ -110,6 +108,15 @@ datasets:
110108
6) Follow download instructions in email from EarthData and put all
111109
files in the same directory
112110
111+
CAMS:
112+
tier: 3
113+
source: https://ads.atmosphere.copernicus.eu/datasets/cams-global-greenhouse-gas-inversion?tab=overview
114+
last_access: 2024-10-28
115+
info: |
116+
You will need to make an account with the ADS data store and accept their licenses to download the data.
117+
Then, select carbon dioxide, surface flux, surface air sample, monthly mean,
118+
as well as the version and years you require to download.
119+
113120
CDS-SATELLITE-ALBEDO:
114121
tier: 3
115122
source: https://cds.climate.copernicus.eu/cdsapp#!/dataset/satellite-albedo?tab=form
@@ -979,7 +986,9 @@ datasets:
979986
980987
MERRA2:
981988
tier: 3
982-
source: https://goldsmr4.gesdisc.eosdis.nasa.gov/data/MERRA2_MONTHLY/ https://goldsmr5.gesdisc.eosdis.nasa.gov/data/MERRA2_MONTHLY/
989+
source: |
990+
https://goldsmr4.gesdisc.eosdis.nasa.gov/data/MERRA2_MONTHLY/
991+
https://goldsmr5.gesdisc.eosdis.nasa.gov/data/MERRA2_MONTHLY/
983992
last_access: 2022-09-13
984993
info: |
985994
Use automatic download. That will download monthly data but with

esmvaltool/cmorizers/data/downloaders/cds.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ def __init__(
4242
dataset_info,
4343
overwrite,
4444
extra_name="",
45+
cds_url="https://cds.climate.copernicus.eu/api",
4546
):
4647
super().__init__(config, dataset, dataset_info, overwrite)
4748
try:
48-
self._client = cdsapi.Client()
49+
self._client = cdsapi.Client(url=cds_url)
4950
except Exception as ex:
5051
if str(ex).endswith(".cdsapirc"):
5152
logger.error(
@@ -96,6 +97,28 @@ def download(
9697
file_path = f"{file_pattern}_{date_str}.{file_format}"
9798
self.download_request(file_path, request_dict)
9899

100+
def download_year(self, year, file_pattern=None, file_format="zip"):
101+
"""Download a specific year from the CDS.
102+
103+
Parameters
104+
----------
105+
year : int
106+
Year to download
107+
file_pattern : str, optional
108+
Filename pattern, by default None
109+
file_format : str, optional
110+
File format, by default tar
111+
"""
112+
request_dict = self._request_dict.copy()
113+
request_dict["year"] = f"{year}"
114+
request_dict["month"] = [f"{m:02d}" for m in range(1, 13)]
115+
116+
os.makedirs(self.local_folder, exist_ok=True)
117+
if file_pattern is None:
118+
file_pattern = f"{self._product_name}"
119+
file_path = f"{file_pattern}_{year}.{file_format}"
120+
self.download_request(file_path, request_dict)
121+
99122
def download_request(self, filename, request=None):
100123
"""Download a specific request.
101124
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Script to download CAMS greenhouse gas data from the Climate Data Store."""
2+
3+
import datetime as dt
4+
5+
from esmvaltool.cmorizers.data.downloaders.cds import CDSDownloader
6+
from esmvaltool.cmorizers.data.utilities import unpack_files_in_folder
7+
8+
9+
def download_dataset(
10+
config, dataset, dataset_info, start_date, end_date, overwrite
11+
):
12+
"""Download dataset.
13+
14+
Parameters
15+
----------
16+
config : dict
17+
ESMValTool's user configuration
18+
dataset : str
19+
Name of the dataset
20+
dataset_info : dict
21+
Dataset information from the datasets.yml file
22+
start_date : datetime
23+
Start of the interval to download
24+
end_date : datetime
25+
End of the interval to download
26+
overwrite : bool
27+
Overwrite already downloaded files
28+
"""
29+
if start_date is None:
30+
start_date = dt.datetime(year=1979, month=1, day=1)
31+
if end_date is None:
32+
end_date = dt.datetime(year=2023, month=12, day=31)
33+
34+
downloader = CDSDownloader(
35+
product_name="cams-global-greenhouse-gas-inversion",
36+
request_dictionary={
37+
"variable": "carbon_dioxide",
38+
"quantity": "surface_flux",
39+
"input_observations": "surface",
40+
"time_aggregation": "monthly_mean",
41+
"version": "v23r1",
42+
},
43+
config=config,
44+
dataset=dataset,
45+
dataset_info=dataset_info,
46+
overwrite=overwrite,
47+
cds_url="https://ads.atmosphere.copernicus.eu/api",
48+
)
49+
50+
for year in range(start_date.year, end_date.year + 1, 1):
51+
downloader.download_year(year)
52+
53+
unpack_files_in_folder(downloader.local_folder)
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""ESMValTool CMORizer for CAMS data.
2+
3+
Tier
4+
Tier 3
5+
6+
Source
7+
https://ads.atmosphere.copernicus.eu/cdsapp#!/dataset/cams-global-greenhouse-gas-inversion?tab=form
8+
9+
Last access
10+
20240909
11+
12+
Download and processing instructions
13+
Select carbon dioxide, surface flux, surface air sample, monthly mean
14+
and download the year you require
15+
16+
"""
17+
18+
import logging
19+
import os
20+
import warnings
21+
from datetime import datetime
22+
23+
import dask.array as da
24+
import iris
25+
from cf_units import Unit
26+
27+
from esmvaltool.cmorizers.data.utilities import (
28+
fix_coords,
29+
fix_var_metadata,
30+
save_variable,
31+
set_global_atts,
32+
set_units,
33+
)
34+
35+
logger = logging.getLogger(__name__)
36+
37+
38+
def _get_time_coord(year, month):
39+
"""Get time coordinate."""
40+
point = datetime(year=year, month=month, day=15)
41+
bound_low = datetime(year=year, month=month, day=1)
42+
if month == 12:
43+
month_bound_up = 1
44+
year_bound_up = year + 1
45+
else:
46+
month_bound_up = month + 1
47+
year_bound_up = year
48+
bound_up = datetime(year=year_bound_up, month=month_bound_up, day=1)
49+
time_units = Unit("days since 1950-01-01 00:00:00", calendar="standard")
50+
time_coord = iris.coords.DimCoord(
51+
time_units.date2num(point),
52+
bounds=time_units.date2num([bound_low, bound_up]),
53+
var_name="time",
54+
standard_name="time",
55+
long_name="time",
56+
units=time_units,
57+
)
58+
return time_coord
59+
60+
61+
def add_timeunits(cube, filename):
62+
"""Add timestamp to cube."""
63+
tmp = str.split(filename, "_")
64+
time_coord = _get_time_coord(int(tmp[-1][:4]), int(tmp[-1][4:6]))
65+
cube.add_aux_coord(time_coord)
66+
67+
68+
def _calculate_flux(cube, filename, area_type):
69+
"""Calculate flux (dividing by land/sea area) and mask land/sea."""
70+
# Get land/sea area fraction
71+
with warnings.catch_warnings():
72+
warnings.filterwarnings(
73+
"ignore",
74+
message="Ignoring netCDF variable '.*?' invalid units '.*?'",
75+
category=UserWarning,
76+
module="iris",
77+
)
78+
lsf_cube = iris.load_cube(filename, "lsf")
79+
lsf = lsf_cube.core_data()
80+
81+
# Mask
82+
if area_type == "land":
83+
mask = lsf == 0.0
84+
elif area_type == "ocean":
85+
mask = lsf > 0
86+
cube.data = da.ma.masked_array(cube.core_data(), mask=mask)
87+
88+
# Calculate flux (sign change since input data and CMOR use different
89+
# conventions)
90+
cube.data = -cube.core_data()
91+
92+
cube.attributes["positive"] = "down"
93+
94+
return cube
95+
96+
97+
def fix_units(cube):
98+
"""Fix units from invalid units through import."""
99+
set_units(cube, "kg m-2 month-1")
100+
cube.convert_units("kg m-2 s-1")
101+
del cube.attributes["invalid_units"]
102+
103+
104+
def extract_variable(short_name, var, filename):
105+
"""Extract variable."""
106+
with warnings.catch_warnings():
107+
warnings.filterwarnings(
108+
"ignore",
109+
message="Ignoring netCDF variable '.*?' invalid units '.*?'",
110+
category=UserWarning,
111+
module="iris",
112+
)
113+
cube = iris.load_cube(filename, var["varname"])
114+
if short_name == "sftof":
115+
cube.data = 100.0 * (1.0 - cube.core_data())
116+
cube.units = "%"
117+
elif short_name == "sftlf":
118+
cube.data = 100.0 * cube.core_data()
119+
cube.units = "%"
120+
elif short_name == "nbp":
121+
_calculate_flux(cube, filename, "land")
122+
add_timeunits(cube, filename)
123+
fix_units(cube)
124+
elif short_name == "fgco2":
125+
_calculate_flux(cube, filename, "ocean")
126+
add_timeunits(cube, filename)
127+
fix_units(cube)
128+
return cube
129+
130+
131+
def _fix_depth(cube, short_name, var, cfg):
132+
"""Fix metadata of cube."""
133+
cmor_info = cfg["cmor_table"].get_variable(var["mip"], short_name)
134+
if "depth0m" in cmor_info.dimensions:
135+
depth_coord = iris.coords.AuxCoord(
136+
0.0,
137+
var_name="depth",
138+
standard_name="depth",
139+
long_name="depth",
140+
units=Unit("m"),
141+
attributes={"positive": "down"},
142+
)
143+
cube.add_aux_coord(depth_coord, ())
144+
145+
146+
def cmorization(in_dir, out_dir, cfg, cfg_user, start_date, end_date):
147+
"""Cmorize data."""
148+
months = [f"{mo:02d}" for mo in range(1, 13)]
149+
fpattern = os.path.join(in_dir, cfg["filename"])
150+
151+
# run the cmorization
152+
for short_name, var in cfg["variables"].items():
153+
var_info = cfg["cmor_table"].get_variable(var["mip"], short_name)
154+
var_cubes = iris.cube.CubeList()
155+
logger.info("CMORizing var %s from file type %s", short_name, fpattern)
156+
# fx files are time invariant
157+
if short_name in ["areacella", "areacello", "sftlf", "sftof"]:
158+
filename = fpattern.format(year=cfg["start_year"], month="01")
159+
cube = extract_variable(short_name, var, filename)
160+
else:
161+
for year in range(cfg["start_year"], cfg["end_year"] + 1):
162+
for month in months:
163+
filename = fpattern.format(year=year, month=month)
164+
var_cubes.append(
165+
extract_variable(short_name, var, filename)
166+
)
167+
168+
cube = var_cubes.merge_cube()
169+
170+
cube.var_name = short_name
171+
fix_coords(cube)
172+
_fix_depth(cube, short_name, var, cfg)
173+
fix_var_metadata(cube, var_info)
174+
attrs = cfg["attributes"]
175+
attrs["mip"] = var["mip"]
176+
set_global_atts(cube, attrs)
177+
178+
save_variable(
179+
cube, short_name, out_dir, attrs, unlimited_dimensions=["time"]
180+
)

esmvaltool/cmorizers/data/utilities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ def unpack_files_in_folder(folder):
585585
continue
586586
if filename.startswith("."):
587587
continue
588-
if not filename.endswith((".gz", ".tgz", ".tar")):
588+
if not filename.endswith((".gz", ".tgz", ".tar", ".zip")):
589589
continue
590590
logger.info("Unpacking %s", filename)
591591
shutil.unpack_archive(full_path, folder)

esmvaltool/recipes/examples/recipe_check_obs.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,17 @@ diagnostics:
11941194
mip: Amon, tier: 3, start_year: 2007, end_year: 2015}
11951195
scripts: null
11961196

1197+
CAMS:
1198+
description: CAMS check
1199+
variables:
1200+
fgco2:
1201+
mip: Omon
1202+
nbp:
1203+
mip: Lmon
1204+
additional_datasets:
1205+
- {dataset: CAMS, project: OBS6, tier: 3,
1206+
type: reanaly, version: v23r1, start_year: 1979, end_year: 2023}
1207+
scripts: null
11971208

11981209
CDS-SATELLITE-ALBEDO:
11991210
description: CDS-SATELLITE-ALBEDO check

esmvaltool/references/cams.bibtex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@Article{cams,
2+
author = {Chevallier, F.},
3+
title = {On the parallelization of atmospheric inversions of CO$_{2}$ surface fluxes within a variational framework},
4+
journal = {Geoscientific Model Development},
5+
volume = {6},
6+
year = {2013},
7+
number = {3},
8+
pages = {783--790},
9+
url = {https://gmd.copernicus.org/articles/6/783/2013/},
10+
doi = {10.5194/gmd-6-783-2013}
11+
}

0 commit comments

Comments
 (0)