Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog/380.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added regional historical changes diagnostics.
4 changes: 3 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ def cmip6_data_catalog(sample_data_dir) -> pd.DataFrame:
@pytest.fixture(scope="session")
def obs4mips_data_catalog(sample_data_dir) -> pd.DataFrame:
adapter = Obs4MIPsDatasetAdapter()
return adapter.find_local_datasets(sample_data_dir / "obs4REF")
obs4ref = adapter.find_local_datasets(sample_data_dir / "obs4REF")
obs4mips = adapter.find_local_datasets(sample_data_dir / "obs4MIPs")
return pd.concat([obs4ref, obs4mips], ignore_index=True)


@pytest.fixture(scope="session")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,10 @@ class Diagnostic(AbstractDiagnostic):
See (climate_ref_example.example.ExampleDiagnostic)[] for an example implementation.
"""

series: Sequence[SeriesDefinition] = tuple()

def __init__(self) -> None:
super().__init__()
self.series = tuple()
self._provider: DiagnosticProvider | None = None

def __repr__(self) -> str:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from collections.abc import Sequence
from pathlib import Path
from typing import Self
from typing import Any, Self

from pydantic import BaseModel, model_validator

Expand All @@ -16,6 +16,9 @@ class SeriesDefinition(BaseModel):
file_pattern: str
"""A glob pattern to match files that contain the series values."""

sel: dict[str, Any] | None = None
"""A dictionary of selection criteria to apply with :meth:`xarray.Dataset.sel` after loading the file."""

dimensions: dict[str, str]
"""Key, value pairs that identify the dimensions of the metric."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
from climate_ref_esmvaltool.diagnostics.ecs import EquilibriumClimateSensitivity
from climate_ref_esmvaltool.diagnostics.enso import ENSOBasicClimatology, ENSOCharacteristics
from climate_ref_esmvaltool.diagnostics.example import GlobalMeanTimeseries
from climate_ref_esmvaltool.diagnostics.regional_historical_changes import (
RegionalHistoricalAnnualCycle,
RegionalHistoricalTimeSeries,
RegionalHistoricalTrend,
)
from climate_ref_esmvaltool.diagnostics.sea_ice_area_basic import SeaIceAreaBasic
from climate_ref_esmvaltool.diagnostics.sea_ice_sensitivity import SeaIceSensitivity
from climate_ref_esmvaltool.diagnostics.tcr import TransientClimateResponse
Expand All @@ -32,6 +37,9 @@
"ENSOCharacteristics",
"EquilibriumClimateSensitivity",
"GlobalMeanTimeseries",
"RegionalHistoricalAnnualCycle",
"RegionalHistoricalTimeSeries",
"RegionalHistoricalTrend",
"SeaIceAreaBasic",
"SeaIceSensitivity",
"TransientClimateResponse",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ def format_result(
"""
return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
def write_recipe(self, definition: ExecutionDefinition) -> Path:
"""
Build the command to run an ESMValTool recipe.
Update the ESMValTool recipe for the diagnostic and write it to file.

Parameters
----------
Expand All @@ -85,16 +85,35 @@ def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
Returns
-------
:
The result of running the diagnostic.
The path to the written recipe.
"""
input_files = {project: definition.datasets[project].datasets for project in definition.datasets}
input_files = {
project: dataset_collection.datasets
for project, dataset_collection in definition.datasets.items()
}
recipe = load_recipe(self.base_recipe)
self.update_recipe(recipe, input_files)

recipe_path = definition.to_output_path("recipe.yml")
with recipe_path.open("w", encoding="utf-8") as file:
yaml.safe_dump(recipe, file, sort_keys=False)
return recipe_path

def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
"""
Build the command to run an ESMValTool recipe.

Parameters
----------
definition
A description of the information needed for this execution of the diagnostic

Returns
-------
:
The result of running the diagnostic.
"""
recipe_path = self.write_recipe(definition)
climate_data = definition.to_output_path("climate_data")

for metric_dataset in definition.datasets.values():
Expand Down Expand Up @@ -137,7 +156,7 @@ def build_cmd(self, definition: ExecutionDefinition) -> Iterable[str]:
{
"OBS": str(data_dir / "OBS"),
"OBS6": str(data_dir / "OBS"),
"native6": str(data_dir / "RAWOBS"),
"native6": str(data_dir / "native6"),
}
)
config["rootpath"]["obs4MIPs"] = [ # type: ignore[index]
Expand Down Expand Up @@ -180,7 +199,7 @@ def build_execution_result(
output_args = CMECOutput.create_template()

# Add the plots and data files
default_series_attributes = (
variable_attributes = (
"long_name",
"standard_name",
"units",
Expand All @@ -203,19 +222,29 @@ def build_execution_result(
}
for series_def in definition.diagnostic.series:
if fnmatch.fnmatch(str(relative_path), f"executions/*/{series_def.file_pattern}"):
dataset = xr.open_dataset(filename)
dataset = xr.open_dataset(
filename, decode_times=xr.coders.CFDatetimeCoder(use_cftime=True)
)
dataset = dataset.sel(series_def.sel)
attributes = {
attr: dataset.attrs[attr]
for attr in (tuple(series_def.attributes) + default_series_attributes)
for attr in series_def.attributes
if attr in dataset.attrs
}
attributes["caption"] = caption
attributes["values_name"] = series_def.values_name
attributes["index_name"] = series_def.index_name
for attr in variable_attributes:
if attr in dataset[series_def.values_name].attrs:
attributes[f"value_{attr}"] = dataset[series_def.values_name].attrs[attr]
if attr in dataset[series_def.index_name].attrs:
attributes[f"index_{attr}"] = dataset[series_def.index_name].attrs[attr]
index = dataset[series_def.index_name].values.tolist()
if hasattr(index[0], "calendar"):
attributes["calendar"] = index[0].calendar
if hasattr(index[0], "isoformat"):
# Convert time objects to strings.
index = [v.isoformat() for v in index]
if hasattr(index[0], "calendar"):
attributes["calendar"] = index[0].calendar

series.append(
SeriesMetricValue(
Expand Down
Loading