Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,11 @@
{
"affiliation": "Met Office, UK",
"name": "Ellis, Hannah"
},
{
"affiliation": "SMHI, Sweden",
"name": "Lenhardt, Julien",
"orcid": "0000-0002-9949-3989"
}
],
"description": "ESMValTool: A community diagnostic and performance metrics tool for routine evaluation of Earth system models in CMIP.",
Expand Down
2 changes: 2 additions & 0 deletions doc/sphinx/source/input.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ A list of the datasets for which a CMORizers is available is provided in the fol
+----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
| GLODAP | dissic, ph, talk (Oyr) | 2 | Python |
+----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
| GLORYS12V1 | uo, vo, thetao, so, zos, tob (Omon), siconc, sithick, siu, siv (SImon), thkcello (Ofx) | 2 | Python |
+----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
| GLWD | wetlandFrac (Emon) | 2 | Python |
+----------------------------------------+------------------------------------------------------------------------------------------------------+------+-----------------+
| GPCC | pr (Amon) | 2 | Python |
Expand Down
44 changes: 44 additions & 0 deletions esmvaltool/cmorizers/data/cmor_config/GLORYS12V1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
# Common global attributes for Cmorizer output
attributes:
dataset_id: GLORYS12V1
version: "v202311"
tier: 2
type: reanaly
project_id: OBS6
source: 'https://data.marine.copernicus.eu/product/GLOBAL_MULTIYEAR_PHY_001_030'
reference: 'glorys12v1'
comment: |
Data is downloaded using the Copernicus Marine Service toolbox API.
start_date: 1993
end_date: 2026

# Variables to cmorize
variables:
uo:
mip: Omon
vo:
mip: Omon
thetao:
mip: Omon
so:
mip: Omon
zos:
mip: Omon
mlotst:
mip: Omon
tob:
mip: Omon
raw_name: bottomT
siconc:
mip: SImon
sithick:
mip: SImon
siu:
mip: SImon
raw_name: usi
siv:
mip: SImon
raw_name: vsi
thkcello:
mip: Ofx
16 changes: 16 additions & 0 deletions esmvaltool/cmorizers/data/datasets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,22 @@ datasets:
last_access: 2020-03-03
info: "Use automatic download feature to get the data"

GLORYS12V1:
tier: 2
source: https://data.marine.copernicus.eu/product/GLOBAL_MULTIYEAR_PHY_001_030/description
last_access: 2026-06-01
info: |
Use automatic download feature to get the data.
The Copernicus Marine Service API is used to download the dataset:
GLORYS12V1 = cmems_mod_glo_phy_my_0.083deg_P1M-m

The dataset starts in 1993 onwards and is frequently updated.
The downloaded data files have the following filename:
mercatorglorys12v1_gl12_mean_{date}.nc

The CMS credentials need to be setup before the download by running:
copernicusmarine login

GLWD:
tier: 2
source: https://figshare.com/articles/dataset/Global_Lakes_and_Wetlands_Database_GLWD_version_2_0/28519994
Expand Down
84 changes: 84 additions & 0 deletions esmvaltool/cmorizers/data/downloaders/cms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Downloader for the Copernicus Marine Service data store."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import copernicusmarine

from .downloader import BaseDownloader

if TYPE_CHECKING:
from pathlib import Path

from esmvaltool.cmorizers.data.typing import DatasetInfo

logger = logging.getLogger(__name__)


class CMSDownloader(BaseDownloader):
"""Downloader class for the COpernicus Marine Service.

Parameters
----------
product_name:
Name of the product in the Copernicus Marine Service data store.
product_filename:
Product filename in the Copernicus Marine Service data store.
original_data_dir:
Directory where original data will be stored.
dataset:
Name of the dataset
dataset_info:
Dataset information from the datasets.yml file
overwrite:
Overwrite already downloaded files
"""

def __init__(
self,
original_data_dir: Path,
product_name: str,
dataset: str,
dataset_info: DatasetInfo,
*,
overwrite: bool,
no_directories: bool,
product_filename: str | None = None,
) -> None:
super().__init__(
original_data_dir=original_data_dir,
dataset=dataset,
dataset_info=dataset_info,
overwrite=overwrite,
)
self._product_name = product_name
self._product_filename = product_filename
self._no_directories = no_directories
if product_filename is None:
self._product_filename = ""

def download(self):
"""Download a whole dataset from the Copernicus Marine Service."""
_ = copernicusmarine.get(
dataset_id=self._product_name,
output_directory=self.local_folder,
no_directories=self._no_directories,
filter=f"{self._product_filename}*.nc",
)

def download_year(self, year):
"""Download a specific year from the Copernicus Marine Service.

Parameters
----------
year : int
Year to download
"""
_ = copernicusmarine.get(
dataset_id=self._product_name,
output_directory=self.local_folder,
no_directories=self._no_directories,
filter=f"{self._product_filename}_{year}*.nc",
)
63 changes: 63 additions & 0 deletions esmvaltool/cmorizers/data/downloaders/datasets/glorys12v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Downloader for the GLORYS12V1 dataset from the Copernicus Marine Service."""

import datetime
import logging

from dateutil import relativedelta

from esmvaltool.cmorizers.data.downloaders.cms import CMSDownloader

logger = logging.getLogger(__name__)


def download_dataset(
original_data_dir,
dataset,
dataset_info,
start_date,
end_date,
overwrite,
):
"""Download dataset.

Parameters
----------
original_data_dir : Path
Directory where original data will be stored.
dataset : str
Name of the dataset
dataset_info : dict
Dataset information from the datasets.yml file
start_date : datetime
Start of the interval to download
end_date : datetime
End of the interval to download
overwrite : bool
Overwrite already downloaded files
"""
# Download variables
downloader = CMSDownloader(
original_data_dir=original_data_dir,
product_name="cmems_mod_glo_phy_my_0.083deg_P1M-m",
dataset=dataset,
dataset_info=dataset_info,
overwrite=overwrite,
no_directories=True,
product_filename="mercatorglorys12v1_gl12_mean",
)
if start_date is None:
start_date = datetime.datetime(1993, 1, 1, tzinfo=datetime.UTC)
if end_date is None:
end_date = datetime.datetime(2026, 4, 30, tzinfo=datetime.UTC)

# Download the whole dataset
if (start_date == datetime.datetime(1993, 1, 1, tzinfo=datetime.UTC)) and (
end_date == datetime.datetime(2026, 4, 30, tzinfo=datetime.UTC)
):
downloader.download()
# Loop over years to download
else:
loop_date = start_date
while loop_date <= end_date:
downloader.download_year(year=loop_date.year)
loop_date += relativedelta.relativedelta(years=1)
161 changes: 161 additions & 0 deletions esmvaltool/cmorizers/data/formatters/datasets/glorys12v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""ESMValTool CMORizer for GLORYS12V1 data.

Tier
Tier 2: other freely-available dataset.

Source
https://data.marine.copernicus.eu/product/GLOBAL_MULTIYEAR_PHY_001_030

Last access
20260601
"""

import datetime
import logging

import iris
import numpy as np

from esmvaltool.cmorizers.data.utilities import (
save_variable,
)

logger = logging.getLogger(__name__)


def _fix_latlon_coordinates(cube):
"""Fix latitude/longitude coordinates."""
for c in ["latitude", "longitude"]:
cube.coord(c).attributes = None
cube.coord(c).var_name = c[:3]


def _fix_depth_coordinate(cube):
"""Fix depth coordinate."""
cube.coord("depth").attributes = {"positive": "down"}
cube.coord("depth").var_name = "lev"
cube.coord("depth").long_name = "Ocean depth coordinate"


def _setup_cell_thickness(cubelist, cfg, out_dir):
"""Set up the cell thickness cube of the thkcello variable."""
logger.info("Creating the cell thickness variable Ofx.thkcello...")
definition = cfg["cmor_table"].get_variable("Ofx", "thkcello")
# Extract a 3D variable to get the depth coordinate
cube_base = cubelist.extract(iris.NameConstraint(var_name="so"))[0]
# Extract a (depth, lat, lon) cube
cube_depth = cube_base[0]
cube_depth.remove_coord("time")
# Creat cell thickness values from the depth coordinate
depth_points = cube_depth.coord("depth").points
thickness_points = np.concatenate(
[[depth_points[0]], np.diff(depth_points)],
)
# Extract a mask from the data following the bathymetry of the original data
mask = np.isnan(cube_depth.data)
# Duplicate the thickness coordinate values along the lat, lon axes
cube_depth.data = np.tile(
thickness_points[..., np.newaxis, np.newaxis],
(1, cube_depth.shape[1], cube_depth.shape[2]),
)
# Mask the cell thickness following the bathymetry
cube_depth.data[mask] = np.nan
# Set up names and attributes
cube_depth.var_name = definition.short_name
cube_depth.standard_name = definition.standard_name
cube_depth.long_name = definition.long_name
cube_depth.units = definition.units
cube_depth.attributes["frequency"] = definition.frequency
cube_depth.attributes["modeling_realm"] = definition.modeling_realm
_fix_latlon_coordinates(cube_depth)
_fix_depth_coordinate(cube_depth)
drop_attrs = ["valid_min", "valid_max", "unit_long"]
for d in drop_attrs:
if d in cube_depth.attributes:
cube_depth.attributes.pop(d)
attributes = cfg["attributes"].copy()
attributes["mip"] = "Ofx"
# Save cube
save_variable(
cube_depth,
cube_depth.var_name,
out_dir,
attributes,
)


def _extract_variable(
short_name,
var,
cubelist,
start_date,
end_date,
cfg,
out_dir,
):
"""Set up the cube for the variable."""
logger.info("Cmorising variable %s.%s...", var["mip"], short_name)
variable = var.get("raw_name", short_name)
cube_var = cubelist.extract(iris.NameConstraint(var_name=variable))
# Equalise attributes
iris.util.equalise_attributes(cube_var)
for cube in cube_var:
cube.attributes.pop("valid_min", None)
cube.attributes.pop("valid_max", None)
# Concatenate cube
cube_var = cube_var.concatenate_cube()
# Extract time range if necesary
time_constraint = iris.Constraint(
time=lambda t: (t >= start_date) and (t <= end_date),
)
cube_var = cube_var.extract(time_constraint)
# Simple fixes
definition = cfg["cmor_table"].get_variable(
var["mip"],
short_name,
)
cube_var.convert_units(definition.units)
cube_var.attributes.pop("unit_long")
cube_var.var_name = short_name
_fix_latlon_coordinates(cube_var)
if cube_var.coords("depth"):
logger.info("Fixing depth coordinate")
_fix_depth_coordinate(cube_var)
attributes = cfg["attributes"].copy()
attributes["mip"] = var["mip"]
# Save cube
save_variable(
cube_var,
cube_var.var_name,
out_dir,
attributes,
unlimited_dimensions=["time"],
)


def cmorization(in_dir, out_dir, cfg, cfg_user, start_date, end_date):
"""Cmorization func call."""
if start_date is None:
start_date = datetime.datetime(1993, 1, 1, tzinfo=datetime.UTC)
if end_date is None:
end_date = datetime.datetime(2026, 4, 30, tzinfo=datetime.UTC)

# Load input in CubeList object
cubelist = iris.load(in_dir / "*.nc")

for short_name, var in cfg["variables"].items():
# If variable is Ofx.thkcello
if short_name == "thkcello":
_setup_cell_thickness(cubelist=cubelist, cfg=cfg, out_dir=out_dir)
# Other variables from Omon or SImon
else:
# Extract variable
_extract_variable(
short_name=short_name,
var=var,
cubelist=cubelist,
start_date=start_date,
end_date=end_date,
cfg=cfg,
out_dir=out_dir,
)
Loading