diff --git a/changelog/51.feature.md b/changelog/51.feature.md new file mode 100644 index 0000000..f9dd69d --- /dev/null +++ b/changelog/51.feature.md @@ -0,0 +1 @@ +Added Equilibrium Climate Sensitivity (ECS) to the ESMValTool metrics package. diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/__init__.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/__init__.py index 6d56daf..80e5ab4 100644 --- a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/__init__.py +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/__init__.py @@ -2,15 +2,12 @@ Rapid evaluating CMIP data with ESMValTool. """ -import importlib.metadata - from ref_core.providers import MetricsProvider -from ref_metrics_esmvaltool.example import GlobalMeanTimeseries - -__version__ = importlib.metadata.version("ref_metrics_esmvaltool") -__core_version__ = importlib.metadata.version("ref_core") +from ref_metrics_esmvaltool._version import __version__ +from ref_metrics_esmvaltool.metrics import EquilibriumClimateSensitivity, GlobalMeanTimeseries # Initialise the metrics manager and register the example metric provider = MetricsProvider("ESMValTool", __version__) provider.register(GlobalMeanTimeseries()) +provider.register(EquilibriumClimateSensitivity()) diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/_version.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/_version.py new file mode 100644 index 0000000..5ace37d --- /dev/null +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/_version.py @@ -0,0 +1,3 @@ +import importlib + +__version__ = importlib.metadata.version("ref_metrics_esmvaltool") diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/example.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/example.py deleted file mode 100644 index 1060c56..0000000 --- a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/example.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import Any - -import xarray -from ref_core.datasets import FacetFilter, SourceDatasetType -from ref_core.metrics import DataRequirement, Metric, MetricExecutionDefinition, MetricResult -from ruamel.yaml import YAML - -from ref_metrics_esmvaltool.recipe import dataframe_to_recipe, load_recipe, run_recipe - -yaml = YAML() - - -def format_cmec_output_bundle(dataset: xarray.Dataset) -> dict[str, Any]: - """ - Create a simple CMEC output bundle for the dataset. - - Parameters - ---------- - dataset - Processed dataset - - Returns - ------- - A CMEC output bundle ready to be written to disk - """ - # TODO: Check how timeseries data are generally serialised - cmec_output = { - "DIMENSIONS": { - "dimensions": { - "source_id": {dataset.attrs["source_id"]: {}}, - "region": {"global": {}}, - "variable": {"tas": {}}, - }, - "json_structure": [ - "model", - "region", - "statistic", - ], - }, - # Is the schema tracked? - "SCHEMA": { - "name": "CMEC-REF", - "package": "example", - "version": "v1", - }, - "RESULTS": { - dataset.attrs["source_id"]: {"global": {"tas": 0}}, - }, - } - - return cmec_output - - -class GlobalMeanTimeseries(Metric): - """ - Calculate the annual mean global mean timeseries for a dataset - """ - - name = "Global Mean Timeseries" - slug = "esmvaltool-global-mean-timeseries" - - data_requirements = ( - DataRequirement( - source_type=SourceDatasetType.CMIP6, - filters=(FacetFilter(facets={"variable_id": ("tas",)}),), - # Add cell areas to the groups - # constraints=(AddCellAreas(),), - # Run the metric on each unique combination of model, variable, experiment, and variant - group_by=("source_id", "variable_id", "experiment_id", "variant_label"), - ), - ) - - def run(self, definition: MetricExecutionDefinition) -> MetricResult: - """ - Run a metric - - Parameters - ---------- - definition - A description of the information needed for this execution of the metric - - Returns - ------- - : - The result of running the metric. - """ - # Load recipe and clear unwanted elements - recipe = load_recipe("examples/recipe_python.yml") - recipe["datasets"].clear() - recipe["diagnostics"].pop("map") - variables = recipe["diagnostics"]["timeseries"]["variables"] - variables.clear() - - # Prepare updated variables section in recipe. - recipe_variables = dataframe_to_recipe(definition.metric_dataset[SourceDatasetType.CMIP6].datasets) - for variable in recipe_variables.values(): - variable["preprocessor"] = "annual_mean_global" - variable["caption"] = "Annual global mean {long_name} according to {dataset}." - - # Populate recipe with new variables/datasets. - variables.update(recipe_variables) - - # Run recipe - result_dir = run_recipe(recipe, definition) - result = next(result_dir.glob("work/timeseries/script1/*.nc")) - annual_mean_global_mean_timeseries = xarray.open_dataset(result) - - return MetricResult.build_from_output_bundle( - definition, format_cmec_output_bundle(annual_mean_global_mean_timeseries) - ) diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/__init__.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/__init__.py new file mode 100644 index 0000000..2c5d804 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/__init__.py @@ -0,0 +1,9 @@ +"""ESMValTool metrics.""" + +from ref_metrics_esmvaltool.metrics.ecs import EquilibriumClimateSensitivity +from ref_metrics_esmvaltool.metrics.example import GlobalMeanTimeseries + +__all__ = [ + "EquilibriumClimateSensitivity", + "GlobalMeanTimeseries", +] diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/base.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/base.py new file mode 100644 index 0000000..0edb9f2 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/base.py @@ -0,0 +1,68 @@ +from abc import abstractmethod +from pathlib import Path +from typing import ClassVar + +import pandas +from ref_core.datasets import SourceDatasetType +from ref_core.metrics import Metric, MetricExecutionDefinition, MetricResult + +from ref_metrics_esmvaltool.recipe import load_recipe, run_recipe +from ref_metrics_esmvaltool.types import OutputBundle, Recipe + + +class ESMValToolMetric(Metric): + """ESMValTool Metric base class.""" + + base_recipe: ClassVar + + @staticmethod + @abstractmethod + def update_recipe(recipe: Recipe, input_files: pandas.DataFrame) -> None: + """ + Update the base recipe for the run. + + Parameters + ---------- + recipe: + The base recipe to update. + input_files: + The dataframe describing the input files. + + """ + + @staticmethod + @abstractmethod + def format_result(result_dir: Path) -> OutputBundle: + """ + Create a CMEC output bundle for the results. + + Parameters + ---------- + result_dir + Directory containing results from an ESMValTool run. + + Returns + ------- + A CMEC output bundle. + """ + + def run(self, definition: MetricExecutionDefinition) -> MetricResult: + """ + Run a metric + + Parameters + ---------- + definition + A description of the information needed for this execution of the metric + + Returns + ------- + : + The result of running the metric. + """ + input_files = definition.metric_dataset[SourceDatasetType.CMIP6].datasets + recipe = load_recipe(self.base_recipe) + self.update_recipe(recipe, input_files) + result_dir = run_recipe(recipe, definition) + output_bundle = self.format_result(result_dir) + return MetricResult.build_from_output_bundle(definition, output_bundle) diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/ecs.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/ecs.py new file mode 100644 index 0000000..95aae64 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/ecs.py @@ -0,0 +1,126 @@ +from pathlib import Path + +import pandas +import xarray +from ref_core.datasets import FacetFilter, SourceDatasetType +from ref_core.metrics import DataRequirement + +from ref_metrics_esmvaltool._version import __version__ +from ref_metrics_esmvaltool.metrics.base import ESMValToolMetric +from ref_metrics_esmvaltool.recipe import dataframe_to_recipe +from ref_metrics_esmvaltool.types import OutputBundle, Recipe + + +class EquilibriumClimateSensitivity(ESMValToolMetric): + """ + Calculate the global mean equilibrium climate sensitivity for a dataset. + """ + + name = "Equilibrium Climate Sensitivity" + slug = "esmvaltool-equilibrium-climate-sensitivity" + base_recipe = "recipe_ecs.yml" + + data_requirements = ( + DataRequirement( + source_type=SourceDatasetType.CMIP6, + filters=( + FacetFilter( + facets={ + "variable_id": ( + "rlut", + "rsdt", + "rsut", + "tas", + ), + "experiment_id": ( + "abrupt-4xCO2", + "piControl", + ), + }, + ), + ), + # TODO: Select only datasets that have both experiments and all four variables + # TODO: Select only datasets that have a contiguous, shared timerange + # TODO: Add cell areas to the groups + # constraints=(AddCellAreas(),), + group_by=("source_id", "variant_label"), + ), + ) + + @staticmethod + def update_recipe(recipe: Recipe, input_files: pandas.DataFrame) -> None: + """Update the recipe.""" + # Only run the diagnostic that computes ECS for a single model. + recipe["diagnostics"] = { + "cmip6": { + "description": "Calculate ECS.", + "variables": { + "tas": { + "preprocessor": "spatial_mean", + }, + "rtnt": { + "preprocessor": "spatial_mean", + "derive": True, + }, + }, + "scripts": { + "ecs": { + "script": "climate_metrics/ecs.py", + "calculate_mmm": False, + }, + }, + }, + } + + # Prepare updated datasets section in recipe. It contains two + # datasets, one for the "abrupt-4xCO2" and one for the "piControl" + # experiment. + recipe_variables = dataframe_to_recipe(input_files) + + # Select a timerange covered by all datasets. + start_times, end_times = [], [] + for variable in recipe_variables.values(): + for dataset in variable["additional_datasets"]: + start, end = dataset["timerange"].split("/") + start_times.append(start) + end_times.append(end) + timerange = f"{max(start_times)}/{min(end_times)}" + + datasets = recipe_variables["tas"]["additional_datasets"] + for dataset in datasets: + dataset["timerange"] = timerange + + recipe["datasets"] = datasets + + @staticmethod + def format_result(result_dir: Path) -> OutputBundle: + """Format the result.""" + ecs_file = result_dir / "work/cmip6/ecs/ecs.nc" + ecs = xarray.open_dataset(ecs_file) + + source_id = ecs.dataset.values[0].decode("utf-8") + cmec_output = { + "DIMENSIONS": { + "dimensions": { + "source_id": {source_id: {}}, + "region": {"global": {}}, + "variable": {"ecs": {}}, + }, + "json_structure": [ + "model", + "region", + "statistic", + ], + }, + # Is the schema tracked? + "SCHEMA": { + "name": "CMEC-REF", + "package": "ref_metrics_esmvaltool", + "version": __version__, + }, + "RESULTS": { + source_id: {"global": {"ecs": ecs.ecs.values[0]}}, + }, + } + + return cmec_output diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/example.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/example.py new file mode 100644 index 0000000..d593680 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/metrics/example.py @@ -0,0 +1,83 @@ +from pathlib import Path + +import pandas +import xarray +from ref_core.datasets import FacetFilter, SourceDatasetType +from ref_core.metrics import DataRequirement + +from ref_metrics_esmvaltool._version import __version__ +from ref_metrics_esmvaltool.metrics.base import ESMValToolMetric +from ref_metrics_esmvaltool.recipe import dataframe_to_recipe +from ref_metrics_esmvaltool.types import OutputBundle, Recipe + + +class GlobalMeanTimeseries(ESMValToolMetric): + """ + Calculate the annual mean global mean timeseries for a dataset. + """ + + name = "Global Mean Timeseries" + slug = "esmvaltool-global-mean-timeseries" + base_recipe = "examples/recipe_python.yml" + + data_requirements = ( + DataRequirement( + source_type=SourceDatasetType.CMIP6, + filters=(FacetFilter(facets={"variable_id": ("tas",)}),), + # Add cell areas to the groups + # constraints=(AddCellAreas(),), + # Run the metric on each unique combination of model, variable, experiment, and variant + group_by=("source_id", "variable_id", "experiment_id", "variant_label"), + ), + ) + + @staticmethod + def update_recipe(recipe: Recipe, input_files: pandas.DataFrame) -> None: + """Update the recipe.""" + # Clear unwanted elements from the recipe. + recipe["datasets"].clear() + recipe["diagnostics"].pop("map") + variables = recipe["diagnostics"]["timeseries"]["variables"] + variables.clear() + + # Prepare updated variables section in recipe. + recipe_variables = dataframe_to_recipe(input_files) + for variable in recipe_variables.values(): + variable["preprocessor"] = "annual_mean_global" + variable["caption"] = "Annual global mean {long_name} according to {dataset}." + + # Populate recipe with new variables/datasets. + variables.update(recipe_variables) + + @staticmethod + def format_result(result_dir: Path) -> OutputBundle: + """Format the result.""" + result = next(result_dir.glob("work/timeseries/script1/*.nc")) + dataset = xarray.open_dataset(result) + + # TODO: Check how timeseries data are generally serialised + cmec_output = { + "DIMENSIONS": { + "dimensions": { + "source_id": {dataset.attrs["source_id"]: {}}, + "region": {"global": {}}, + "variable": {"tas": {}}, + }, + "json_structure": [ + "model", + "region", + "statistic", + ], + }, + # Is the schema tracked? + "SCHEMA": { + "name": "CMEC-REF", + "package": "ref_metrics_esmvaltool", + "version": __version__, + }, + "RESULTS": { + dataset.attrs["source_id"]: {"global": {"tas": 0}}, + }, + } + + return cmec_output diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipe.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipe.py index 37ad5f5..ef8a15d 100644 --- a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipe.py +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipe.py @@ -10,6 +10,8 @@ from ref_core.metrics import MetricExecutionDefinition from ruamel.yaml import YAML +from ref_metrics_esmvaltool.types import Recipe + if TYPE_CHECKING: import pandas as pd @@ -89,13 +91,13 @@ def as_facets( return facets -def dataframe_to_recipe(datasets: pd.DataFrame) -> dict[str, Any]: +def dataframe_to_recipe(files: pd.DataFrame) -> dict[str, Any]: """Convert the datasets dataframe to a recipe "variables" section. Parameters ---------- - datasets - The pandas dataframe describing the input datasets. + files + The pandas dataframe describing the input files. Returns ------- @@ -103,7 +105,7 @@ def dataframe_to_recipe(datasets: pd.DataFrame) -> dict[str, Any]: """ variables: dict[str, Any] = {} # TODO: refine to make it possible to combine historical and scenario runs. - for _, group in datasets.groupby("instance_id"): + for _, group in files.groupby("instance_id"): facets = as_facets(group) short_name = facets.pop("short_name") if short_name not in variables: @@ -120,11 +122,10 @@ def dataframe_to_recipe(datasets: pd.DataFrame) -> dict[str, Any]: version=_ESMVALTOOL_VERSION, env="REF_METRICS_ESMVALTOOL_DATA_DIR", ) -with importlib.resources.files("ref_metrics_esmvaltool").joinpath("recipes.txt").open("rb") as _file: - _RECIPES.load_registry(_file) +_RECIPES.load_registry(importlib.resources.open_binary("ref_metrics_esmvaltool", "recipes.txt")) -def load_recipe(recipe: str) -> dict[str, Any]: +def load_recipe(recipe: str) -> Recipe: """Load a recipe. Parameters @@ -165,7 +166,7 @@ def prepare_climate_data(datasets: pd.DataFrame, climate_data_dir: Path) -> None tgt.symlink_to(row.path) -def run_recipe(recipe: dict[str, Any], definition: MetricExecutionDefinition) -> Path: +def run_recipe(recipe: Recipe, definition: MetricExecutionDefinition) -> Path: """Run an ESMValTool recipe. Parameters @@ -175,10 +176,16 @@ def run_recipe(recipe: dict[str, Any], definition: MetricExecutionDefinition) -> definition A description of the information needed for this execution of the metric. + Returns + ------- + : + Directory containing results from the ESMValTool run. + """ output_dir = definition.output_fragment + output_dir.mkdir(parents=True, exist_ok=True) - recipe_path = output_dir / "recipe_example.yml" + recipe_path = output_dir / "recipe.yml" with recipe_path.open("w", encoding="utf-8") as file: yaml.dump(recipe, file) diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipes.txt b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipes.txt index 66600a8..9ccd5b6 100644 --- a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipes.txt +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/recipes.txt @@ -1 +1,2 @@ examples/recipe_python.yml ab3f06d269bb2c1368f4dc39da9bcb232fb2adb1fa556ba769e6c16294ffb4a3 +recipe_ecs.yml 0cc57034fcb64e32015b4ff949ece5df8cdb8c6f493618b50ceded119fb37918 diff --git a/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/types.py b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/types.py new file mode 100644 index 0000000..0252c14 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/src/ref_metrics_esmvaltool/types.py @@ -0,0 +1,5 @@ +from typing import Any + +Recipe = dict[str, Any] + +OutputBundle = dict[str, Any] diff --git a/packages/ref-metrics-esmvaltool/tests/__init__.py b/packages/ref-metrics-esmvaltool/tests/__init__.py new file mode 100644 index 0000000..9407087 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the ref-metrics-esmvaltool package.""" diff --git a/packages/ref-metrics-esmvaltool/tests/unit/__init__.py b/packages/ref-metrics-esmvaltool/tests/unit/__init__.py new file mode 100644 index 0000000..fdbc331 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests for the ref-metrics-esmvaltool package.""" diff --git a/packages/ref-metrics-esmvaltool/tests/unit/metrics/__init__.py b/packages/ref-metrics-esmvaltool/tests/unit/metrics/__init__.py new file mode 100644 index 0000000..5f355d0 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/tests/unit/metrics/__init__.py @@ -0,0 +1 @@ +"""Tests for the metrics provided by the package.""" diff --git a/packages/ref-metrics-esmvaltool/tests/unit/metrics/input_files_ecs.json b/packages/ref-metrics-esmvaltool/tests/unit/metrics/input_files_ecs.json new file mode 100644 index 0000000..ecb6c3a --- /dev/null +++ b/packages/ref-metrics-esmvaltool/tests/unit/metrics/input_files_ecs.json @@ -0,0 +1,362 @@ +{ + "start_time": { + "108": "0101-01-16T12:00:00.000", + "109": "0101-01-16T12:00:00.000", + "110": "0101-01-16T12:00:00.000", + "111": "0101-01-16T12:00:00.000", + "20": "0101-01-16T12:00:00.000", + "21": "0101-01-16T12:00:00.000", + "22": "0101-01-16T12:00:00.000", + "23": "0101-01-16T12:00:00.000" + }, + "end_time": { + "108": "0600-12-16T12:00:00.000", + "109": "0600-12-16T12:00:00.000", + "110": "0600-12-16T12:00:00.000", + "111": "0600-12-16T12:00:00.000", + "20": "0250-12-16T12:00:00.000", + "21": "0250-12-16T12:00:00.000", + "22": "0250-12-16T12:00:00.000", + "23": "0250-12-16T12:00:00.000" + }, + "path": { + "108": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/piControl/r1i1p1f1/Amon/rlut/gn/v20210316/rlut_Amon_ACCESS-ESM1-5_piControl_r1i1p1f1_gn_010101-060012.nc", + "109": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/piControl/r1i1p1f1/Amon/rsdt/gn/v20210316/rsdt_Amon_ACCESS-ESM1-5_piControl_r1i1p1f1_gn_010101-060012.nc", + "110": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/piControl/r1i1p1f1/Amon/rsut/gn/v20210316/rsut_Amon_ACCESS-ESM1-5_piControl_r1i1p1f1_gn_010101-060012.nc", + "111": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/piControl/r1i1p1f1/Amon/tas/gn/v20210316/tas_Amon_ACCESS-ESM1-5_piControl_r1i1p1f1_gn_010101-060012.nc", + "20": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/abrupt-4xCO2/r1i1p1f1/Amon/rlut/gn/v20191115/rlut_Amon_ACCESS-ESM1-5_abrupt-4xCO2_r1i1p1f1_gn_010101-025012.nc", + "21": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/abrupt-4xCO2/r1i1p1f1/Amon/rsdt/gn/v20191115/rsdt_Amon_ACCESS-ESM1-5_abrupt-4xCO2_r1i1p1f1_gn_010101-025012.nc", + "22": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/abrupt-4xCO2/r1i1p1f1/Amon/rsut/gn/v20191115/rsut_Amon_ACCESS-ESM1-5_abrupt-4xCO2_r1i1p1f1_gn_010101-025012.nc", + "23": "/home/bandela/climate_data/CMIP6/CMIP/CSIRO/ACCESS-ESM1-5/abrupt-4xCO2/r1i1p1f1/Amon/tas/gn/v20191115/tas_Amon_ACCESS-ESM1-5_abrupt-4xCO2_r1i1p1f1_gn_010101-025012.nc" + }, + "activity_id": { + "108": "CMIP", + "109": "CMIP", + "110": "CMIP", + "111": "CMIP", + "20": "CMIP", + "21": "CMIP", + "22": "CMIP", + "23": "CMIP" + }, + "branch_method": { + "108": "standard", + "109": "standard", + "110": "standard", + "111": "standard", + "20": "standard", + "21": "standard", + "22": "standard", + "23": "standard" + }, + "branch_time_in_child": { + "108": 0.0, + "109": 0.0, + "110": 0.0, + "111": 0.0, + "20": 0.0, + "21": 0.0, + "22": 0.0, + "23": 0.0 + }, + "branch_time_in_parent": { + "108": 36524.0, + "109": 36524.0, + "110": 36524.0, + "111": 36524.0, + "20": 0.0, + "21": 0.0, + "22": 0.0, + "23": 0.0 + }, + "experiment": { + "108": "pre-industrial control", + "109": "pre-industrial control", + "110": "pre-industrial control", + "111": "pre-industrial control", + "20": "abrupt quadrupling of CO2", + "21": "abrupt quadrupling of CO2", + "22": "abrupt quadrupling of CO2", + "23": "abrupt quadrupling of CO2" + }, + "experiment_id": { + "108": "piControl", + "109": "piControl", + "110": "piControl", + "111": "piControl", + "20": "abrupt-4xCO2", + "21": "abrupt-4xCO2", + "22": "abrupt-4xCO2", + "23": "abrupt-4xCO2" + }, + "frequency": { + "108": "mon", + "109": "mon", + "110": "mon", + "111": "mon", + "20": "mon", + "21": "mon", + "22": "mon", + "23": "mon" + }, + "grid": { + "108": "native atmosphere N96 grid (145x192 latxlon)", + "109": "native atmosphere N96 grid (145x192 latxlon)", + "110": "native atmosphere N96 grid (145x192 latxlon)", + "111": "native atmosphere N96 grid (145x192 latxlon)", + "20": "native atmosphere N96 grid (145x192 latxlon)", + "21": "native atmosphere N96 grid (145x192 latxlon)", + "22": "native atmosphere N96 grid (145x192 latxlon)", + "23": "native atmosphere N96 grid (145x192 latxlon)" + }, + "grid_label": { + "108": "gn", + "109": "gn", + "110": "gn", + "111": "gn", + "20": "gn", + "21": "gn", + "22": "gn", + "23": "gn" + }, + "institution_id": { + "108": "CSIRO", + "109": "CSIRO", + "110": "CSIRO", + "111": "CSIRO", + "20": "CSIRO", + "21": "CSIRO", + "22": "CSIRO", + "23": "CSIRO" + }, + "nominal_resolution": { + "108": "250 km", + "109": "250 km", + "110": "250 km", + "111": "250 km", + "20": "250 km", + "21": "250 km", + "22": "250 km", + "23": "250 km" + }, + "parent_activity_id": { + "108": "CMIP", + "109": "CMIP", + "110": "CMIP", + "111": "CMIP", + "20": "CMIP", + "21": "CMIP", + "22": "CMIP", + "23": "CMIP" + }, + "parent_experiment_id": { + "108": "piControl-spinup", + "109": "piControl-spinup", + "110": "piControl-spinup", + "111": "piControl-spinup", + "20": "piControl", + "21": "piControl", + "22": "piControl", + "23": "piControl" + }, + "parent_source_id": { + "108": "ACCESS-ESM1-5", + "109": "ACCESS-ESM1-5", + "110": "ACCESS-ESM1-5", + "111": "ACCESS-ESM1-5", + "20": "ACCESS-ESM1-5", + "21": "ACCESS-ESM1-5", + "22": "ACCESS-ESM1-5", + "23": "ACCESS-ESM1-5" + }, + "parent_time_units": { + "108": "days since 0001-01-01", + "109": "days since 0001-01-01", + "110": "days since 0001-01-01", + "111": "days since 0001-01-01", + "20": "days since 0101-01-01", + "21": "days since 0101-01-01", + "22": "days since 0101-01-01", + "23": "days since 0101-01-01" + }, + "parent_variant_label": { + "108": "r1i1p1f1", + "109": "r1i1p1f1", + "110": "r1i1p1f1", + "111": "r1i1p1f1", + "20": "r1i1p1f1", + "21": "r1i1p1f1", + "22": "r1i1p1f1", + "23": "r1i1p1f1" + }, + "product": { + "108": "model-output", + "109": "model-output", + "110": "model-output", + "111": "model-output", + "20": "model-output", + "21": "model-output", + "22": "model-output", + "23": "model-output" + }, + "realm": { + "108": "atmos", + "109": "atmos", + "110": "atmos", + "111": "atmos", + "20": "atmos", + "21": "atmos", + "22": "atmos", + "23": "atmos" + }, + "source_id": { + "108": "ACCESS-ESM1-5", + "109": "ACCESS-ESM1-5", + "110": "ACCESS-ESM1-5", + "111": "ACCESS-ESM1-5", + "20": "ACCESS-ESM1-5", + "21": "ACCESS-ESM1-5", + "22": "ACCESS-ESM1-5", + "23": "ACCESS-ESM1-5" + }, + "source_type": { + "108": "AOGCM", + "109": "AOGCM", + "110": "AOGCM", + "111": "AOGCM", + "20": "AOGCM", + "21": "AOGCM", + "22": "AOGCM", + "23": "AOGCM" + }, + "sub_experiment": { + "108": "none", + "109": "none", + "110": "none", + "111": "none", + "20": "none", + "21": "none", + "22": "none", + "23": "none" + }, + "sub_experiment_id": { + "108": "none", + "109": "none", + "110": "none", + "111": "none", + "20": "none", + "21": "none", + "22": "none", + "23": "none" + }, + "table_id": { + "108": "Amon", + "109": "Amon", + "110": "Amon", + "111": "Amon", + "20": "Amon", + "21": "Amon", + "22": "Amon", + "23": "Amon" + }, + "variable_id": { + "108": "rlut", + "109": "rsdt", + "110": "rsut", + "111": "tas", + "20": "rlut", + "21": "rsdt", + "22": "rsut", + "23": "tas" + }, + "variant_label": { + "108": "r1i1p1f1", + "109": "r1i1p1f1", + "110": "r1i1p1f1", + "111": "r1i1p1f1", + "20": "r1i1p1f1", + "21": "r1i1p1f1", + "22": "r1i1p1f1", + "23": "r1i1p1f1" + }, + "member_id": { + "108": "r1i1p1f1", + "109": "r1i1p1f1", + "110": "r1i1p1f1", + "111": "r1i1p1f1", + "20": "r1i1p1f1", + "21": "r1i1p1f1", + "22": "r1i1p1f1", + "23": "r1i1p1f1" + }, + "standard_name": { + "108": "toa_outgoing_longwave_flux", + "109": "toa_incoming_shortwave_flux", + "110": "toa_outgoing_shortwave_flux", + "111": "air_temperature", + "20": "toa_outgoing_longwave_flux", + "21": "toa_incoming_shortwave_flux", + "22": "toa_outgoing_shortwave_flux", + "23": "air_temperature" + }, + "long_name": { + "108": "TOA Outgoing Longwave Radiation", + "109": "TOA Incident Shortwave Radiation", + "110": "TOA Outgoing Shortwave Radiation", + "111": "Near-Surface Air Temperature", + "20": "TOA Outgoing Longwave Radiation", + "21": "TOA Incident Shortwave Radiation", + "22": "TOA Outgoing Shortwave Radiation", + "23": "Near-Surface Air Temperature" + }, + "units": { + "108": "W m-2", + "109": "W m-2", + "110": "W m-2", + "111": "K", + "20": "W m-2", + "21": "W m-2", + "22": "W m-2", + "23": "K" + }, + "vertical_levels": { + "108": 1, + "109": 1, + "110": 1, + "111": 1, + "20": 1, + "21": 1, + "22": 1, + "23": 1 + }, + "init_year": { + "108": null, + "109": null, + "110": null, + "111": null, + "20": null, + "21": null, + "22": null, + "23": null + }, + "version": { + "108": "v20210316", + "109": "v20210316", + "110": "v20210316", + "111": "v20210316", + "20": "v20191115", + "21": "v20191115", + "22": "v20191115", + "23": "v20191115" + }, + "instance_id": { + "108": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.piControl.r1i1p1f1.Amon.rlut.gn.v20210316", + "109": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.piControl.r1i1p1f1.Amon.rsdt.gn.v20210316", + "110": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.piControl.r1i1p1f1.Amon.rsut.gn.v20210316", + "111": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.piControl.r1i1p1f1.Amon.tas.gn.v20210316", + "20": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.abrupt-4xCO2.r1i1p1f1.Amon.rlut.gn.v20191115", + "21": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.abrupt-4xCO2.r1i1p1f1.Amon.rsdt.gn.v20191115", + "22": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.abrupt-4xCO2.r1i1p1f1.Amon.rsut.gn.v20191115", + "23": "CMIP6.CMIP.CSIRO.ACCESS-ESM1-5.abrupt-4xCO2.r1i1p1f1.Amon.tas.gn.v20191115" + } +} diff --git a/packages/ref-metrics-esmvaltool/tests/unit/metrics/test_ecs.py b/packages/ref-metrics-esmvaltool/tests/unit/metrics/test_ecs.py new file mode 100644 index 0000000..3461cb8 --- /dev/null +++ b/packages/ref-metrics-esmvaltool/tests/unit/metrics/test_ecs.py @@ -0,0 +1,37 @@ +from pathlib import Path + +import numpy as np +import pandas +import xarray as xr +from ref_metrics_esmvaltool.metrics import EquilibriumClimateSensitivity +from ref_metrics_esmvaltool.recipe import load_recipe + + +def test_update_recipe(): + input_files = pandas.read_json(Path(__file__).parent / "input_files_ecs.json") + print(input_files[["start_time"]]) + recipe = load_recipe("recipe_ecs.yml") + EquilibriumClimateSensitivity().update_recipe(recipe, input_files) + assert len(recipe["datasets"]) == 2 + assert len(recipe["diagnostics"]) == 1 + assert set(recipe["diagnostics"]["cmip6"]["variables"]) == {"tas", "rtnt"} + + +def test_format_output(tmp_path): + ecs = xr.Dataset( + data_vars={ + "ecs": (["dim0"], np.array([1.0])), + }, + coords={ + "dataset": ("dim0", np.array([b"abc"])), + }, + ) + result_dir = tmp_path + subdir = result_dir / "work" / "cmip6" / "ecs" + subdir.mkdir(parents=True) + ecs.to_netcdf(subdir / "ecs.nc") + + output_bundle = EquilibriumClimateSensitivity().format_result(result_dir) + + assert isinstance(output_bundle, dict) + assert output_bundle["RESULTS"]["abc"]["global"]["ecs"] == 1.0 diff --git a/packages/ref-metrics-esmvaltool/tests/unit/test_metrics.py b/packages/ref-metrics-esmvaltool/tests/unit/test_metrics.py index 9b0beb7..72d745a 100644 --- a/packages/ref-metrics-esmvaltool/tests/unit/test_metrics.py +++ b/packages/ref-metrics-esmvaltool/tests/unit/test_metrics.py @@ -2,7 +2,7 @@ import ref_metrics_esmvaltool from ref_core.datasets import DatasetCollection, MetricDataset, SourceDatasetType from ref_core.metrics import MetricExecutionDefinition -from ref_metrics_esmvaltool.example import GlobalMeanTimeseries +from ref_metrics_esmvaltool.metrics import GlobalMeanTimeseries @pytest.fixture @@ -28,7 +28,7 @@ def test_example_metric(tmp_path, mocker, metric_dataset, cmip6_data_catalog): definition = MetricExecutionDefinition( output_directory=output_directory, output_fragment=tmp_path, - key="global_mean_timeseries", + key="esmvaltool-global-mean-timeseries", metric_dataset=MetricDataset( { SourceDatasetType.CMIP6: DatasetCollection(ds, "instance_id"), @@ -51,7 +51,7 @@ def mock_check_call(cmd, *args, **kwargs): side_effect=mock_check_call, ) open_dataset = mocker.patch.object( - ref_metrics_esmvaltool.example.xarray, + ref_metrics_esmvaltool.metrics.example.xarray, "open_dataset", autospec=True, spec_set=True, diff --git a/packages/ref-metrics-esmvaltool/tests/unit/test_provider.py b/packages/ref-metrics-esmvaltool/tests/unit/test_provider.py index d777576..3dd38da 100644 --- a/packages/ref-metrics-esmvaltool/tests/unit/test_provider.py +++ b/packages/ref-metrics-esmvaltool/tests/unit/test_provider.py @@ -1,10 +1,4 @@ -from ref_metrics_esmvaltool import __core_version__, __version__, provider - - -# Placeholder to get CI working -def test_version(): - assert __version__ == "0.1.0" - assert __core_version__ == "0.1.0" +from ref_metrics_esmvaltool import __version__, provider def test_provider(): @@ -12,4 +6,4 @@ def test_provider(): assert provider.slug == "esmvaltool" assert provider.version == __version__ - assert len(provider) == 1 + assert len(provider) == 2