Skip to content

climate_ref_esmvaltool.diagnostics #

ESMValTool diagnostics.

ClimateAtGlobalWarmingLevels #

Bases: ESMValToolDiagnostic

Calculate climate variables at global warming levels.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/climate_at_global_warming_levels.py
class ClimateAtGlobalWarmingLevels(ESMValToolDiagnostic):
    """
    Calculate climate variables at global warming levels.
    """

    name = "Climate variables at global warming levels"
    slug = "climate-at-global-warming-levels"
    base_recipe = "recipe_calculate_gwl_exceedance_stats.yml"

    variables = (
        "pr",
        "tas",
    )

    matching_facets = (
        "source_id",
        "member_id",
        "grid_label",
        "table_id",
        "variable_id",
    )

    cmip7_matching_facets = (
        "source_id",
        "variant_label",
        "grid_label",
        "variable_id",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": variables,
                            "experiment_id": (
                                "ssp126",
                                "ssp245",
                                "ssp370",
                                "ssp585",
                            ),
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("experiment_id",),
                constraints=(
                    AddSupplementaryDataset(
                        supplementary_facets={"experiment_id": "historical"},
                        matching_facets=matching_facets,
                        optional_matching_facets=tuple(),
                    ),
                    RequireTimerange(
                        group_by=matching_facets,
                        start=PartialDateTime(year=1850, month=1),
                        end=PartialDateTime(year=2100, month=12),
                    ),
                    RequireFacets(
                        "experiment_id",
                        required_facets=("historical",),
                        group_by=matching_facets,
                    ),
                    RequireFacets(
                        "variable_id",
                        required_facets=variables,
                        group_by=("experiment_id", "source_id", "member_id", "grid_label", "table_id"),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "experiment_id": (
                                # TODO: Redetermine the scenario naming for CMIP7 and update these accordingly
                            ),
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("experiment_id",),
                constraints=(
                    AddSupplementaryDataset(
                        supplementary_facets={"experiment_id": "historical"},
                        matching_facets=cmip7_matching_facets,
                        optional_matching_facets=tuple(),
                    ),
                    RequireTimerange(
                        group_by=cmip7_matching_facets,
                        start=PartialDateTime(year=1850, month=1),
                        end=PartialDateTime(year=2100, month=12),
                    ),
                    RequireFacets(
                        "experiment_id",
                        required_facets=("historical",),
                        group_by=cmip7_matching_facets,
                    ),
                    RequireFacets(
                        "variable_id",
                        required_facets=variables,
                        group_by=("experiment_id", "source_id", "variant_label", "grid_label"),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
    )
    facets = ("experiment_id", "global_warming_level", "metric")

    files = tuple(
        FileDefinition(
            file_pattern=f"plots/gwl_mean_plots_{var_name}/plot_gwl_stats/*.png",
            dimensions={
                "statistic": "mean",
                "variable_id": var_name,
            },
        )
        for var_name in variables
    ) + tuple(
        FileDefinition(
            file_pattern=f"work/gwl_mean_plots_{var_name}/plot_gwl_stats/*.nc",
            dimensions={
                "statistic": "mean",
                "variable_id": var_name,
            },
        )
        for var_name in variables
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["ssp245", "historical"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "pr", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1850", "2100"),
                    ),
                ),
            ),
            # Disabling test until we have scenarios available for CMIP7
            # TestCase(
            #     name="cmip7",
            #     description="Test with CMIP7 data.",
            #     requests=(
            #         CMIP7Request(
            #             slug="cmip7",
            #             facets={
            #                 "experiment_id": ["ssp245", "historical"],
            #                 "source_id": "CanESM5",
            #                 "variable_id": ["areacella", "pr", "tas"],
            #                 "branded_variable": [
            #                     "areacella_ti-u-hxy-u",
            #                     "pr_tavg-u-hxy-u",
            #                     "tas_tavg-h2m-hxy-u",
            #                 ],
            #                 "variant_label": "r1i1p1f1",
            #                 "frequency": ["fx", "mon"],
            #                 "region": "glb",
            #             },
            #             remove_ensembles=True,
            #             time_span=("1850", "2100"),
            #         ),
            #     ),
            # ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pd.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Set up the datasets
        cmip_source = get_cmip_source_type(input_files)
        diagnostics = recipe["diagnostics"]
        for diagnostic in diagnostics.values():
            diagnostic.pop("additional_datasets")
        group_by: tuple[str, ...]
        if cmip_source == SourceDatasetType.CMIP7:
            group_by = (
                "source_id",
                "variant_label",
                "grid_label",
                "variable_id",
            )
        else:
            group_by = (
                "source_id",
                "member_id",
                "grid_label",
                "table_id",
                "variable_id",
            )
        recipe_variables = dataframe_to_recipe(
            input_files[cmip_source],
            group_by=group_by,
        )
        datasets = recipe_variables["tas"]["additional_datasets"]
        datasets = [ds for ds in datasets if ds["exp"] != "historical"]
        for dataset in datasets:
            dataset.pop("timerange")
        recipe["datasets"] = datasets

        # Specify the timeranges
        diagnostics["calculate_gwl_exceedance_years"]["variables"]["tas_anomaly"] = {
            "short_name": "tas",
            "preprocessor": "calculate_anomalies",
            "timerange": "1850/2100",
        }

        diagnostics["gwl_mean_plots_tas"]["variables"]["tas"] = {
            "short_name": "tas",
            "preprocessor": "multi_model_gwl_stats",
            "timerange": "2000/2100",
        }

        diagnostics["gwl_mean_plots_pr"]["variables"]["pr"] = {
            "short_name": "pr",
            "preprocessor": "multi_model_gwl_stats",
            "timerange": "2000/2100",
        }

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": [
                "global_warming_level",
                "metric",
            ],
            "global_warming_level": {},
            "metric": {"exceedance_year": {}},
        }

        df = pd.read_csv(
            result_dir
            / "work"
            / "calculate_gwl_exceedance_years"
            / "gwl_exceedance_calculation"
            / "GWL_exceedance_years.csv"
        )
        for row in df.itertuples(index=False):
            gwl = str(row.GWL)
            if gwl not in metric_args[MetricCV.DIMENSIONS.value]["global_warming_level"]:
                metric_args[MetricCV.DIMENSIONS.value]["global_warming_level"][gwl] = {}
            metric_args[MetricCV.RESULTS.value][gwl] = {
                "exceedance_year": int(str(row.Exceedance_Year)),
            }

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/climate_at_global_warming_levels.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": [
            "global_warming_level",
            "metric",
        ],
        "global_warming_level": {},
        "metric": {"exceedance_year": {}},
    }

    df = pd.read_csv(
        result_dir
        / "work"
        / "calculate_gwl_exceedance_years"
        / "gwl_exceedance_calculation"
        / "GWL_exceedance_years.csv"
    )
    for row in df.itertuples(index=False):
        gwl = str(row.GWL)
        if gwl not in metric_args[MetricCV.DIMENSIONS.value]["global_warming_level"]:
            metric_args[MetricCV.DIMENSIONS.value]["global_warming_level"][gwl] = {}
        metric_args[MetricCV.RESULTS.value][gwl] = {
            "exceedance_year": int(str(row.Exceedance_Year)),
        }

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/climate_at_global_warming_levels.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pd.DataFrame],
) -> None:
    """Update the recipe."""
    # Set up the datasets
    cmip_source = get_cmip_source_type(input_files)
    diagnostics = recipe["diagnostics"]
    for diagnostic in diagnostics.values():
        diagnostic.pop("additional_datasets")
    group_by: tuple[str, ...]
    if cmip_source == SourceDatasetType.CMIP7:
        group_by = (
            "source_id",
            "variant_label",
            "grid_label",
            "variable_id",
        )
    else:
        group_by = (
            "source_id",
            "member_id",
            "grid_label",
            "table_id",
            "variable_id",
        )
    recipe_variables = dataframe_to_recipe(
        input_files[cmip_source],
        group_by=group_by,
    )
    datasets = recipe_variables["tas"]["additional_datasets"]
    datasets = [ds for ds in datasets if ds["exp"] != "historical"]
    for dataset in datasets:
        dataset.pop("timerange")
    recipe["datasets"] = datasets

    # Specify the timeranges
    diagnostics["calculate_gwl_exceedance_years"]["variables"]["tas_anomaly"] = {
        "short_name": "tas",
        "preprocessor": "calculate_anomalies",
        "timerange": "1850/2100",
    }

    diagnostics["gwl_mean_plots_tas"]["variables"]["tas"] = {
        "short_name": "tas",
        "preprocessor": "multi_model_gwl_stats",
        "timerange": "2000/2100",
    }

    diagnostics["gwl_mean_plots_pr"]["variables"]["pr"] = {
        "short_name": "pr",
        "preprocessor": "multi_model_gwl_stats",
        "timerange": "2000/2100",
    }

ClimateDriversForFire #

Bases: ESMValToolDiagnostic

Calculate diagnostics regarding climate drivers for fire.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/climate_drivers_for_fire.py
class ClimateDriversForFire(ESMValToolDiagnostic):
    """
    Calculate diagnostics regarding climate drivers for fire.
    """

    name = "Climate drivers for fire"
    slug = "climate-drivers-for-fire"
    base_recipe = "ref/recipe_ref_fire.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        {
                            "variable_id": ("hurs", "pr", "tas", "tasmax"),
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        }
                    ),
                    FacetFilter(
                        {
                            "variable_id": ("cVeg", "treeFrac"),
                            "experiment_id": "historical",
                            "table_id": "Lmon",
                        }
                    ),
                    FacetFilter(
                        {
                            "variable_id": "vegFrac",
                            "experiment_id": "historical",
                            "table_id": "Emon",
                        }
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(2013, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("sftlf", SourceDatasetType.CMIP6),
                    RequireFacets(
                        "variable_id",
                        (
                            "cVeg",
                            "hurs",
                            "pr",
                            "tas",
                            "tasmax",
                            "sftlf",
                            "treeFrac",
                            "vegFrac",
                        ),
                    ),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        {
                            "branded_variable": (
                                "hurs_tavg-h2m-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "tas_tmaxavg-h2m-hxy-u",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        }
                    ),
                    FacetFilter(
                        {
                            "branded_variable": (
                                "cVeg_tavg-u-hxy-lnd",
                                "treeFrac_tavg-u-hxy-u",
                                "vegFrac_tavg-u-hxy-u",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        }
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(2013, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("sftlf", SourceDatasetType.CMIP7),
                    RequireFacets(
                        "branded_variable",
                        (
                            "cVeg_tavg-u-hxy-lnd",
                            "hurs_tavg-h2m-hxy-u",
                            "pr_tavg-u-hxy-u",
                            "sftlf_ti-u-hxy-u",
                            "tas_tavg-h2m-hxy-u",
                            "tas_tmaxavg-h2m-hxy-u",
                            "treeFrac_tavg-u-hxy-u",
                            "vegFrac_tavg-u-hxy-u",
                        ),
                    ),
                ),
            ),
        ),
    )
    facets = ()
    files = (
        FileDefinition(
            file_pattern="plots/fire_evaluation/fire_evaluation/burnt_fraction_*.png",
            dimensions={"statistic": "burnt fraction"},
        ),
        FileDefinition(
            file_pattern="plots/fire_evaluation/fire_evaluation/fire_weather_control_*.png",
            dimensions={"statistic": "fire weather control"},
        ),
        FileDefinition(
            file_pattern="plots/fire_evaluation/fire_evaluation/fuel_load_continuity_control_*.png",
            dimensions={"statistic": "fuel load continuity control"},
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": [
                                "cVeg",
                                "hurs",
                                "pr",
                                "sftlf",
                                "tas",
                                "tasmax",
                                "treeFrac",
                                "vegFrac",
                            ],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("2013", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": [
                                "cVeg",
                                "hurs",
                                "pr",
                                "sftlf",
                                "tas",
                                "tasmax",
                                "treeFrac",
                                "vegFrac",
                            ],
                            "branded_variable": [
                                "cVeg_tavg-u-hxy-lnd",
                                "hurs_tavg-h2m-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "sftlf_ti-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "tas_tmaxavg-h2m-hxy-u",
                                "treeFrac_tavg-u-hxy-u",
                                "vegFrac_tavg-u-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("2013", "2014"),
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        cmip_source = get_cmip_source_type(input_files)
        recipe_variables = dataframe_to_recipe(input_files[cmip_source])
        recipe.pop("datasets")
        for diagnostic in recipe["diagnostics"].values():
            for variable_group, variable in diagnostic.get("variables", {}).items():
                cmip6_short_name = variable.get("short_name", variable_group)
                if cmip_source == SourceDatasetType.CMIP7 and cmip6_short_name == "tasmax":
                    short_name = "tas"
                else:
                    short_name = cmip6_short_name
                variable["short_name"] = short_name
                variable["start_year"] = 2013
                variable["end_year"] = 2014
                datasets = recipe_variables[short_name]["additional_datasets"]
                for dataset in datasets:
                    dataset.pop("timerange", None)
                if cmip_source == SourceDatasetType.CMIP7 and short_name == "tas":
                    # Separate the two "tas" datasets into "tas" and "tasmax".
                    if cmip6_short_name == "tasmax":
                        datasets = [d for d in datasets if d["branding_suffix"] == "tmaxavg-h2m-hxy-u"]
                    else:
                        datasets = [d for d in datasets if d["branding_suffix"] == "tavg-h2m-hxy-u"]

                variable["additional_datasets"] = datasets

        recipe["diagnostics"]["fire_evaluation"]["scripts"]["fire_evaluation"]["remove_confire_files"] = True

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/climate_drivers_for_fire.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    cmip_source = get_cmip_source_type(input_files)
    recipe_variables = dataframe_to_recipe(input_files[cmip_source])
    recipe.pop("datasets")
    for diagnostic in recipe["diagnostics"].values():
        for variable_group, variable in diagnostic.get("variables", {}).items():
            cmip6_short_name = variable.get("short_name", variable_group)
            if cmip_source == SourceDatasetType.CMIP7 and cmip6_short_name == "tasmax":
                short_name = "tas"
            else:
                short_name = cmip6_short_name
            variable["short_name"] = short_name
            variable["start_year"] = 2013
            variable["end_year"] = 2014
            datasets = recipe_variables[short_name]["additional_datasets"]
            for dataset in datasets:
                dataset.pop("timerange", None)
            if cmip_source == SourceDatasetType.CMIP7 and short_name == "tas":
                # Separate the two "tas" datasets into "tas" and "tasmax".
                if cmip6_short_name == "tasmax":
                    datasets = [d for d in datasets if d["branding_suffix"] == "tmaxavg-h2m-hxy-u"]
                else:
                    datasets = [d for d in datasets if d["branding_suffix"] == "tavg-h2m-hxy-u"]

            variable["additional_datasets"] = datasets

    recipe["diagnostics"]["fire_evaluation"]["scripts"]["fire_evaluation"]["remove_confire_files"] = True

CloudRadiativeEffects #

Bases: ESMValToolDiagnostic

Plot climatologies and zonal mean profiles of cloud radiative effects (sw + lw) for a dataset.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_radiative_effects.py
class CloudRadiativeEffects(ESMValToolDiagnostic):
    """
    Plot climatologies and zonal mean profiles of cloud radiative effects (sw + lw) for a dataset.
    """

    name = "Climatologies and zonal mean profiles of cloud radiative effects"
    slug = "cloud-radiative-effects"
    base_recipe = "ref/recipe_ref_cre.yml"

    variables = (
        "rlut",
        "rlutcs",
        "rsut",
        "rsutcs",
    )
    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": variables,
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        }
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1996, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    RequireFacets("variable_id", variables),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "rlut_tavg-u-hxy-u",
                                "rlutcs_tavg-u-hxy-u",
                                "rsut_tavg-u-hxy-u",
                                "rsutcs_tavg-u-hxy-u",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        }
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1996, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    RequireFacets("variable_id", variables),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
        # TODO: Use CERES-EBAF, ESACCI-CLOUD, and ISCCP-FH from obs4MIPs once available.
    )

    facets = ()
    files = (
        tuple(
            FileDefinition(
                file_pattern=f"plots/plot_profiles/plot/variable_vs_lat_{var_name}_*.png",
                dimensions={"variable_id": var_name, "statistic": "zonal mean"},
            )
            for var_name in ["lwcre", "swcre"]
        )
        + tuple(
            FileDefinition(
                file_pattern=f"plots/plot_maps/plot/map_{var_name}_*.png",
                dimensions={"variable_id": var_name, "statistic": "climatology map"},
            )
            for var_name in ["lwcre", "swcre"]
        )
        + tuple(
            FileDefinition(
                file_pattern=f"work/plot_maps/plot/map_{var_name}_*.nc",
                dimensions={"variable_id": var_name, "statistic": "climatology map"},
            )
            for var_name in ["lwcre", "swcre"]
        )
    )
    series = tuple(
        SeriesDefinition(
            file_pattern=f"plot_profiles/plot/variable_vs_lat_{var_name}_*.nc",
            sel={"dim0": 0},  # Select the model.
            dimensions={"variable_id": var_name, "statistic": "zonal mean"},
            values_name=var_name,
            index_name="lat",
            attributes=[],
        )
        for var_name in ["lwcre", "swcre"]
    ) + tuple(
        SeriesDefinition(
            file_pattern=f"plot_profiles/plot/variable_vs_lat_{var_name}_*.nc",
            sel={"dim0": i},  # Select the observation.
            dimensions={"variable_id": var_name, "statistic": "zonal mean", "reference_source_id": source_id},
            values_name=var_name,
            index_name="lat",
            attributes=[],
        )
        for var_name in ["lwcre", "swcre"]
        for i, source_id in enumerate(
            ["CERES-EBAF-Ed4.2", "ESACCI-CLOUD-AVHRR-AMPM-fv3.0", "ISCCP-FH"], start=1
        )
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "rlut", "rlutcs", "rsut", "rsutcs"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "rlut", "rlutcs", "rsut", "rsutcs"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "rlut_tavg-u-hxy-u",
                                "rlutcs_tavg-u-hxy-u",
                                "rsut_tavg-u-hxy-u",
                                "rsutcs_tavg-u-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(recipe: Recipe, input_files: dict[SourceDatasetType, pandas.DataFrame]) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}

        datasets = recipe_variables["rsut"]["additional_datasets"]
        for dataset in datasets:
            dataset.pop("timerange")
        recipe["datasets"] = datasets

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_radiative_effects.py
@staticmethod
def update_recipe(recipe: Recipe, input_files: dict[SourceDatasetType, pandas.DataFrame]) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}

    datasets = recipe_variables["rsut"]["additional_datasets"]
    for dataset in datasets:
        dataset.pop("timerange")
    recipe["datasets"] = datasets

CloudScatterplotCliTa #

Bases: ESMValToolDiagnostic

Scatterplot of cli vs ta.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py
class CloudScatterplotCliTa(ESMValToolDiagnostic):
    """
    Scatterplot of cli vs ta.
    """

    name = "Scatterplots of two cloud-relevant variables (cli vs ta)"
    slug = "cloud-scatterplots-cli-ta"
    base_recipe = "ref/recipe_ref_scatterplot.yml"
    facets = ()
    data_requirements = get_cmip_data_requirements(
        ("cli", "ta"),
        branded_variables=("cli_tavg-al-hxy-u", "ta_tavg-p19-hxy-air"),
    )
    update_recipe = partial(update_recipe, var_x="cli", var_y="ta")
    files = (
        FileDefinition(
            file_pattern="plots/plot_joint_cli_ta_model/plot/png/*.png",
            dimensions={"statistic": "joint histogram of cli vs ta"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_cli_ta_model/plot/*.nc",
            dimensions={"statistic": "joint histogram of cli vs ta"},
        ),
    )
    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CESM2",
                            "variable_id": ["areacella", "cli", "ta"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CESM2",
                            "variable_id": ["areacella", "cli", "ta"],
                            "table_id": ["fx", "Amon"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "cli_tavg-al-hxy-u",
                                "ta_tavg-p19-hxy-air",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
        )
    )

CloudScatterplotCliviLwcre #

Bases: ESMValToolDiagnostic

Scatterplot of clivi vs lwcre.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py
class CloudScatterplotCliviLwcre(ESMValToolDiagnostic):
    """
    Scatterplot of clivi vs lwcre.
    """

    name = "Scatterplots of two cloud-relevant variables (clivi vs lwcre)"
    slug = "cloud-scatterplots-clivi-lwcre"
    base_recipe = "ref/recipe_ref_scatterplot.yml"
    facets = ()
    data_requirements = get_cmip_data_requirements(
        ("clivi", "rlut", "rlutcs"),
        branded_variables=("clivi_tavg-u-hxy-u", "rlut_tavg-u-hxy-u", "rlutcs_tavg-u-hxy-u"),
    )
    update_recipe = partial(update_recipe, var_x="clivi", var_y="lwcre")
    files = (
        FileDefinition(
            file_pattern="plots/plot_joint_clivi_lwcre_model/plot/png/*.png",
            dimensions={"statistic": "joint histogram of clivi vs lwcre"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_clivi_lwcre_model/plot/*.nc",
            dimensions={"statistic": "joint histogram of clivi vs lwcre"},
        ),
    )
    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "clivi", "rlut", "rlutcs"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "clivi", "rlut", "rlutcs"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "clivi_tavg-u-hxy-u",
                                "rlut_tavg-u-hxy-u",
                                "rlutcs_tavg-u-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
        )
    )

CloudScatterplotCltSwcre #

Bases: ESMValToolDiagnostic

Scatterplot of clt vs swcre.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py
class CloudScatterplotCltSwcre(ESMValToolDiagnostic):
    """
    Scatterplot of clt vs swcre.
    """

    name = "Scatterplots of two cloud-relevant variables (clt vs swcre)"
    slug = "cloud-scatterplots-clt-swcre"
    base_recipe = "ref/recipe_ref_scatterplot.yml"
    facets = ()
    data_requirements = get_cmip_data_requirements(
        ("clt", "rsut", "rsutcs"),
        branded_variables=("clt_tavg-u-hxy-u", "rsut_tavg-u-hxy-u", "rsutcs_tavg-u-hxy-u"),
    )
    update_recipe = partial(update_recipe, var_x="clt", var_y="swcre")
    files = (
        FileDefinition(
            file_pattern="plots/plot_joint_clt_swcre_model/plot/png/*.png",
            dimensions={"statistic": "joint histogram of clt vs swcre"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_clt_swcre_model/plot/*.nc",
            dimensions={"statistic": "joint histogram of clt vs swcre"},
        ),
    )
    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "clt", "rsut", "rsutcs"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "clt", "rsut", "rsutcs"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "clt_tavg-u-hxy-u",
                                "rsut_tavg-u-hxy-u",
                                "rsutcs_tavg-u-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
        )
    )

CloudScatterplotClwviPr #

Bases: ESMValToolDiagnostic

Scatterplot of clwvi vs pr.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py
class CloudScatterplotClwviPr(ESMValToolDiagnostic):
    """
    Scatterplot of clwvi vs pr.
    """

    name = "Scatterplots of two cloud-relevant variables (clwvi vs pr)"
    slug = "cloud-scatterplots-clwvi-pr"
    base_recipe = "ref/recipe_ref_scatterplot.yml"
    facets = ()
    data_requirements = get_cmip_data_requirements(
        ("clwvi", "pr"),
        branded_variables=("clwvi_tavg-u-hxy-u", "pr_tavg-u-hxy-u"),
    )
    update_recipe = partial(update_recipe, var_x="clwvi", var_y="pr")
    files = (
        FileDefinition(
            file_pattern="plots/plot_joint_clwvi_pr_model/plot/png/*.png",
            dimensions={"statistic": "joint histogram of clwvi vs pr"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_clwvi_pr_model/plot/*.nc",
            dimensions={"statistic": "joint histogram of clwvi vs pr"},
        ),
    )
    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "clwvi", "pr"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "clwvi", "pr"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "clwvi_tavg-u-hxy-u",
                                "pr_tavg-u-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2014"),
                    ),
                ),
            ),
        )
    )

CloudScatterplotsReference #

Bases: ESMValToolDiagnostic

Reference scatterplots of two cloud-relevant variables.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py
class CloudScatterplotsReference(ESMValToolDiagnostic):
    """
    Reference scatterplots of two cloud-relevant variables.
    """

    name = "Reference scatterplots of two cloud-relevant variables"
    slug = "cloud-scatterplots-reference"
    base_recipe = "ref/recipe_ref_scatterplot.yml"
    facets = ()
    files = (
        FileDefinition(
            file_pattern="plots/plot_joint_cli_ta_ref/plot/png/*.png",
            dimensions={"statistic": "joint histogram of cli vs ta"},
        ),
        FileDefinition(
            file_pattern="plots/plot_joint_clivi_lwcre_ref/plot/png/*.png",
            dimensions={"statistic": "joint histogram of clivi vs lwcre"},
        ),
        FileDefinition(
            file_pattern="plots/plot_joint_clt_swcre_ref/plot/png/*.png",
            dimensions={"statistic": "joint histogram of clt vs swcre"},
        ),
        FileDefinition(
            file_pattern="plots/plot_joint_clwvi_pr_ref/plot/png/*.png",
            dimensions={"statistic": "joint histogram of clwvi vs pr"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_cli_ta_ref/plot/*.nc",
            dimensions={"statistic": "joint histogram of cli vs ta"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_clivi_lwcre_ref/plot/*.nc",
            dimensions={"statistic": "joint histogram of clivi vs lwcre"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_clt_swcre_ref/plot/*.nc",
            dimensions={"statistic": "joint histogram of clt vs swcre"},
        ),
        FileDefinition(
            file_pattern="work/plot_joint_clwvi_pr_ref/plot/*.nc",
            dimensions={"statistic": "joint histogram of clwvi vs pr"},
        ),
    )
    data_requirements = (
        DataRequirement(
            source_type=SourceDatasetType.obs4MIPs,
            filters=(
                FacetFilter(
                    facets={
                        "source_id": ("ERA-5",),
                        "variable_id": ("ta",),
                    },
                ),
            ),
            group_by=("instance_id",),
            constraints=(
                RequireTimerange(
                    group_by=("instance_id",),
                    start=PartialDateTime(2007, 1),
                    end=PartialDateTime(2014, 12),
                ),
            ),
            # TODO: Add obs4MIPs datasets once available and working:
            #
            # obs4MIPs datasets with issues:
            # - GPCP-V2.3: pr
            # - CERES-EBAF-4-2: rlut, rlutcs, rsut, rsutcs
            #
            # Unsure if available on obs4MIPs:
            # - AVHRR-AMPM-fv3.0: clivi, clwvi
            # - ESACCI-CLOUD: clt
            # - CALIPSO-ICECLOUD: cli
            #
            # Related issues:
            # - https://github.com/Climate-REF/climate-ref/issues/260
            # - https://github.com/esMValGroup/esMValCore/issues/2712
            # - https://github.com/esMValGroup/esMValCore/issues/2711
            # - https://github.com/sciTools/iris/issues/6411
        ),
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.obs4MIPs])
        recipe["diagnostics"] = {k: v for k, v in recipe["diagnostics"].items() if k.endswith("_ref")}

        era5_dataset = recipe_variables["ta"]["additional_datasets"][0]
        era5_dataset["timerange"] = "2007/2015"  # Use the same timerange as for the other variable.
        era5_dataset["alias"] = era5_dataset["dataset"]
        diagnostic = recipe["diagnostics"]["plot_joint_cli_ta_ref"]
        diagnostic["variables"]["ta"]["additional_datasets"] = [era5_dataset]
        suptitle = "CALIPSO-ICECLOUD / {dataset} {timerange}".format(**era5_dataset)
        diagnostic["scripts"]["plot"]["suptitle"] = suptitle
        diagnostic["scripts"]["plot"]["plot_filename"] = (
            f"jointplot_cli_ta_{suptitle.replace(' ', '_').replace('/', '-')}"
        )

        # Use the correct obs4MIPs dataset name for dataset that cannot be ingested
        # https://github.com/Climate-REF/climate-ref/issues/260.
        diagnostic = recipe["diagnostics"]["plot_joint_clwvi_pr_ref"]
        diagnostic["variables"]["pr"]["additional_datasets"] = [
            {
                "dataset": "GPCP-V2.3",
                "project": "obs4MIPs",
                "alias": "GPCP-SG",
                "timerange": "1992/2016",
            }
        ]

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/cloud_scatterplots.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[SourceDatasetType.obs4MIPs])
    recipe["diagnostics"] = {k: v for k, v in recipe["diagnostics"].items() if k.endswith("_ref")}

    era5_dataset = recipe_variables["ta"]["additional_datasets"][0]
    era5_dataset["timerange"] = "2007/2015"  # Use the same timerange as for the other variable.
    era5_dataset["alias"] = era5_dataset["dataset"]
    diagnostic = recipe["diagnostics"]["plot_joint_cli_ta_ref"]
    diagnostic["variables"]["ta"]["additional_datasets"] = [era5_dataset]
    suptitle = "CALIPSO-ICECLOUD / {dataset} {timerange}".format(**era5_dataset)
    diagnostic["scripts"]["plot"]["suptitle"] = suptitle
    diagnostic["scripts"]["plot"]["plot_filename"] = (
        f"jointplot_cli_ta_{suptitle.replace(' ', '_').replace('/', '-')}"
    )

    # Use the correct obs4MIPs dataset name for dataset that cannot be ingested
    # https://github.com/Climate-REF/climate-ref/issues/260.
    diagnostic = recipe["diagnostics"]["plot_joint_clwvi_pr_ref"]
    diagnostic["variables"]["pr"]["additional_datasets"] = [
        {
            "dataset": "GPCP-V2.3",
            "project": "obs4MIPs",
            "alias": "GPCP-SG",
            "timerange": "1992/2016",
        }
    ]

ENSOBasicClimatology #

Bases: ESMValToolDiagnostic

Calculate the ENSO CLIVAR metrics - background climatology.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/enso.py
class ENSOBasicClimatology(ESMValToolDiagnostic):
    """
    Calculate the ENSO CLIVAR metrics - background climatology.
    """

    name = "ENSO Basic Climatology"
    slug = "enso-basic-climatology"
    base_recipe = "ref/recipe_enso_basicclimatology.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": ("pr", "tauu"),
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        },
                    ),
                    FacetFilter(
                        facets={
                            "variable_id": "tos",
                            "experiment_id": "historical",
                            "table_id": "Omon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    RequireFacets(
                        "variable_id",
                        (
                            "pr",
                            "tauu",
                            "tos",
                        ),
                    ),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "pr_tavg-u-hxy-u",
                                "tauu_tavg-u-hxy-u",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                    FacetFilter(
                        facets={
                            "branded_variable": "tos_tavg-u-hxy-sea",
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    RequireFacets(
                        "variable_id",
                        (
                            "pr",
                            "tauu",
                            "tos",
                        ),
                    ),
                ),
            ),
        ),
    )
    facets = ()

    files = (
        tuple(
            FileDefinition(
                file_pattern=f"plots/diagnostic_metrics/plot_script/png/*_eq_{var_name}_bias.png",
                dimensions={
                    "statistic": (
                        f"zonal bias in the time-mean {var_name} structure across the equatorial Pacific"
                    ),
                },
            )
            for var_name in ("pr", "sst", "tauu")
        )
        + tuple(
            FileDefinition(
                file_pattern=f"plots/diagnostic_metrics/plot_script/png/*_eq_{var_name}_seacycle.png",
                dimensions={
                    "statistic": (
                        "zonal bias in the amplitude of the mean seasonal cycle of "
                        f"{var_name} in the equatorial Pacific"
                    ),
                },
            )
            for var_name in ("pr", "sst", "tauu")
        )
        + (
            FileDefinition(
                file_pattern="plots/diagnostic_metrics/plot_script/png/*_pr_double.png",
                dimensions={
                    "statistic": "meridional bias in the time-mean pr structure across the eastern Pacific",
                },
            ),
            FileDefinition(
                file_pattern="plots/diagnostic_metrics/plot_script/png/*_pr_double_seacycle.png",
                dimensions={
                    "statistic": (
                        "meridional bias in the amplitude of the mean seasonal "
                        "pr cycle in the eastern Pacific"
                    ),
                },
            ),
        )
        + tuple(
            FileDefinition(
                file_pattern=f"plots/diagnostic_level2/plot_script/png/*_{var_name}_map_*.png",
                dimensions={
                    "variable_id": "tos" if var_name == "tos" else var_name,
                },
            )
            for var_name in ("pr", "tauu", "tos")
        )
    )

    series = (
        tuple(
            SeriesDefinition(
                file_pattern=f"diagnostic_metrics/plot_script/{source_id}_eq_{var_name}_bias.nc",
                dimensions=(
                    {
                        "statistic": (
                            f"zonal bias in the time-mean {var_name} structure across the equatorial Pacific"
                        ),
                    }
                    | ({} if source_id == "{source_id}" else {"reference_source_id": source_id})
                ),
                values_name="tos" if var_name == "sst" else var_name,
                index_name="lon",
                attributes=[],
            )
            for var_name in ("pr", "sst", "tauu")
            for source_id in ("{source_id}", "GPCP-V2.3", "TROPFLUX")
        )
        + tuple(
            SeriesDefinition(
                file_pattern=f"diagnostic_metrics/plot_script/{{source_id}}_eq_{var_name}_seacycle.nc",
                dimensions=(
                    {
                        "statistic": (
                            "zonal bias in the amplitude of the mean seasonal cycle of "
                            f"{var_name} in the equatorial Pacific"
                        ),
                    }
                    | ({} if source_id == "{source_id}" else {"reference_source_id": source_id})
                ),
                values_name="tos" if var_name == "sst" else var_name,
                index_name="lon",
                attributes=[],
            )
            for var_name in ("pr", "sst", "tauu")
            for source_id in ("{source_id}", "GPCP-V2.3", "TROPFLUX")
        )
        + tuple(
            SeriesDefinition(
                file_pattern="diagnostic_metrics/plot_script/{source_id}_pr_double.nc",
                dimensions=(
                    {
                        "statistic": (
                            "meridional bias in the time-mean pr structure across the eastern Pacific"
                        ),
                    }
                    | ({} if source_id == "{source_id}" else {"reference_source_id": source_id})
                ),
                values_name="pr",
                index_name="lat",
                attributes=[],
            )
            for source_id in ("{source_id}", "GPCP-V2.3")
        )
        + tuple(
            SeriesDefinition(
                file_pattern="diagnostic_metrics/plot_script/*_pr_double_seacycle.nc",
                dimensions=(
                    {
                        "statistic": (
                            "meridional bias in the amplitude of the mean seasonal "
                            "pr cycle in the eastern Pacific"
                        ),
                    }
                    | ({} if source_id == "{source_id}" else {"reference_source_id": source_id})
                ),
                values_name="pr",
                index_name="lat",
                attributes=[],
            )
            for source_id in ("{source_id}", "GPCP-V2.3")
        )
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["pr", "tauu", "tos"],
                            "frequency": "mon",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["pr", "tauu", "tos"],
                            "branded_variable": [
                                "pr_tavg-u-hxy-u",
                                "tauu_tavg-u-hxy-u",
                                "tos_tavg-u-hxy-sea",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": "mon",
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        recipe.pop("datasets")
        for diagnostic in recipe["diagnostics"].values():
            for variable in diagnostic["variables"].values():
                variable["additional_datasets"].extend(
                    recipe_variables[variable["short_name"]]["additional_datasets"]
                )

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/enso.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    recipe.pop("datasets")
    for diagnostic in recipe["diagnostics"].values():
        for variable in diagnostic["variables"].values():
            variable["additional_datasets"].extend(
                recipe_variables[variable["short_name"]]["additional_datasets"]
            )

ENSOCharacteristics #

Bases: ESMValToolDiagnostic

Calculate the ENSO CLIVAR metrics - basic ENSO characteristics.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/enso.py
class ENSOCharacteristics(ESMValToolDiagnostic):
    """
    Calculate the ENSO CLIVAR metrics - basic ENSO characteristics.
    """

    name = "ENSO Characteristics"
    slug = "enso-characteristics"
    base_recipe = "ref/recipe_enso_characteristics.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": "tos",
                            "experiment_id": "historical",
                            "table_id": "Omon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP6),
                    RequireFacets("variable_id", ("tos", "areacello")),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": "tos_tavg-u-hxy-sea",
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP7),
                    RequireFacets("variable_id", ("tos", "areacello")),
                ),
            ),
        ),
    )
    facets = ("grid_label", "member_id", "source_id", "region", "metric")
    # ENSO pattern and lifecycle are series, but the ESMValTool diagnostic
    # script does not save the values used in the figure.
    series = tuple()
    files = tuple(
        FileDefinition(
            file_pattern=f"plots/diagnostic_metrics/plot_script/png/*_{metric}.png",
            dimensions={"metric": metric},
        )
        for metric in (
            "09pattern",
            "10lifecycle",
            "11amplitude",
            "12seasonality",
            "13asymmetry",
            "14duration",
            "15diversity",
        )
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacello", "tos"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacello", "tos"],
                            "branded_variable": [
                                "areacello_ti-u-hxy-u",
                                "tos_tavg-u-hxy-sea",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        recipe["datasets"] = recipe_variables["tos"]["additional_datasets"]
        # TODO: update the observational data requirement once available on ESGF.
        # Observations - use only one per run
        recipe["datasets"].append(
            # {
            #     "dataset": "NOAA-ERSSTv5",
            #     "version": "v5",
            #     "project": "OBS6",
            #     "type": "reanaly",
            #     "tier": 2,
            # }
            {
                "dataset": "TROPFLUX",
                "version": "v1",
                "project": "OBS6",
                "type": "reanaly",
                "tier": 2,
                "mip": "Omon",
            }
        )

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        metrics = pd.read_csv(
            result_dir / "work" / "diagnostic_metrics" / "plot_script" / "matrix.csv",
            names=["dataset", "metric_name", "metric_value"],
        )

        # Update the diagnostic bundle arguments with the computed diagnostics.
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": [
                "region",
                "metric",
            ],
            "region": {"global": {}},
            "metric": {metric: {} for metric in metrics.metric_name},
        }
        metric_args[MetricCV.RESULTS.value] = {
            "global": {
                metric_name: metric_value
                for metric_name, metric_value in zip(
                    metrics.metric_name,
                    metrics.metric_value,
                )
            },
        }

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/enso.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    metrics = pd.read_csv(
        result_dir / "work" / "diagnostic_metrics" / "plot_script" / "matrix.csv",
        names=["dataset", "metric_name", "metric_value"],
    )

    # Update the diagnostic bundle arguments with the computed diagnostics.
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": [
            "region",
            "metric",
        ],
        "region": {"global": {}},
        "metric": {metric: {} for metric in metrics.metric_name},
    }
    metric_args[MetricCV.RESULTS.value] = {
        "global": {
            metric_name: metric_value
            for metric_name, metric_value in zip(
                metrics.metric_name,
                metrics.metric_value,
            )
        },
    }

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/enso.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    recipe["datasets"] = recipe_variables["tos"]["additional_datasets"]
    # TODO: update the observational data requirement once available on ESGF.
    # Observations - use only one per run
    recipe["datasets"].append(
        # {
        #     "dataset": "NOAA-ERSSTv5",
        #     "version": "v5",
        #     "project": "OBS6",
        #     "type": "reanaly",
        #     "tier": 2,
        # }
        {
            "dataset": "TROPFLUX",
            "version": "v1",
            "project": "OBS6",
            "type": "reanaly",
            "tier": 2,
            "mip": "Omon",
        }
    )

EquilibriumClimateSensitivity #

Bases: ESMValToolDiagnostic

Calculate the global mean equilibrium climate sensitivity for a dataset.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ecs.py
class EquilibriumClimateSensitivity(ESMValToolDiagnostic):
    """
    Calculate the global mean equilibrium climate sensitivity for a dataset.
    """

    name = "Equilibrium Climate Sensitivity"
    slug = "equilibrium-climate-sensitivity"
    base_recipe = "recipe_ecs.yml"

    variables = (
        "rlut",
        "rsdt",
        "rsut",
        "tas",
    )
    experiments = (
        "abrupt-4xCO2",
        "piControl",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": variables,
                            "experiment_id": "abrupt-4xCO2",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    AddParentDataset.from_defaults(SourceDatasetType.CMIP6),
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireFacets(
                        "variable_id",
                        required_facets=variables,
                        group_by=("source_id", "member_id", "grid_label", "experiment_id"),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "rlut_tavg-u-hxy-u",
                                "rsdt_tavg-u-hxy-u",
                                "rsut_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ),
                            "experiment_id": "abrupt-4xCO2",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireOverlappingTimerange(group_by=("instance_id",)),
                    AddParentDataset.from_defaults(SourceDatasetType.CMIP7),
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireFacets(
                        "variable_id",
                        required_facets=variables,
                        group_by=("source_id", "variant_label", "grid_label", "experiment_id"),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
    )
    facets = ("grid_label", "member_id", "source_id", "region", "metric")
    series = (
        SeriesDefinition(
            file_pattern="ecs/calculate/ecs_regression_*.nc",
            dimensions={
                "statistic": ("global annual mean anomaly of rtnt vs tas"),
            },
            values_name="rtnt_anomaly",
            index_name="tas_anomaly",
            attributes=[],
        ),
    )
    files = (
        FileDefinition(
            file_pattern="plots/ecs/calculate/*.png",
            dimensions={"statistic": "global annual mean anomaly of rtnt vs tas"},
        ),
        FileDefinition(
            file_pattern="work/ecs/calculate/ecs.nc",
            dimensions={"metric": "ecs"},
        ),
        FileDefinition(
            file_pattern="work/ecs/calculate/lambda.nc",
            dimensions={"metric": "lambda"},
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["abrupt-4xCO2", "piControl"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "rlut", "rsdt", "rsut", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["abrupt-4xCO2", "piControl"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "rlut", "rsdt", "rsut", "tas"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "rlut_tavg-u-hxy-u",
                                "rsdt_tavg-u-hxy-u",
                                "rsut_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Only run the diagnostic that computes ECS for a single model.
        recipe["diagnostics"] = {
            "ecs": {
                "description": "Calculate ECS.",
                "variables": {
                    "tas": {
                        "preprocessor": "spatial_mean",
                    },
                    "rtnt": {
                        "preprocessor": "spatial_mean",
                        "derive": True,
                    },
                },
                "scripts": {
                    "calculate": {
                        "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.
        cmip_source = get_cmip_source_type(input_files)
        if cmip_source == SourceDatasetType.CMIP6:
            df = input_files[SourceDatasetType.CMIP6]
            recipe["datasets"] = get_child_and_parent_dataset(
                df[df.variable_id == "tas"],
                parent_experiment="piControl",
                child_duration_in_years=150,
                parent_offset_in_years=0,
                parent_duration_in_years=150,
            )
        else:
            # CMIP7: use per-variable additional_datasets to preserve correct branding_suffix
            recipe_variables = dataframe_to_recipe(
                input_files[cmip_source],
                equalize_timerange=True,
            )
            recipe["datasets"] = []
            for var_name, var_settings in recipe["diagnostics"]["ecs"]["variables"].items():
                short_name = var_settings.get("short_name", var_name)
                if short_name in recipe_variables:
                    var_settings["additional_datasets"] = recipe_variables[short_name]["additional_datasets"]
                elif var_name == "rtnt":
                    # rtnt is derived from rlut, rsdt, rsut - use rlut's dataset
                    var_settings["additional_datasets"] = recipe_variables["rlut"]["additional_datasets"]

        # Remove keys from the recipe that are only used for YAML anchors
        keys_to_remove = [
            "CMIP5_RTMT",
            "CMIP6_RTMT",
            "CMIP5_RTNT",
            "CMIP6_RTNT",
            "ECS_SCRIPT",
            "SCATTERPLOT",
        ]
        for key in keys_to_remove:
            recipe.pop(key, None)

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        ecs_ds = xarray.open_dataset(result_dir / "work" / "ecs" / "calculate" / "ecs.nc")
        ecs = float(fillvalues_to_nan(ecs_ds["ecs"].values)[0])
        lambda_ds = xarray.open_dataset(result_dir / "work" / "ecs" / "calculate" / "lambda.nc")
        lambda_ = float(fillvalues_to_nan(lambda_ds["lambda"].values)[0])

        # Update the diagnostic bundle arguments with the computed diagnostics.
        metric_args[MetricCV.DIMENSIONS.value] = {
            MetricCV.JSON_STRUCTURE.value: [
                "region",
                "metric",
            ],
            "region": {"global": {}},
            "metric": {"ecs": {}, "lambda": {}},
        }
        metric_args[MetricCV.RESULTS.value] = {
            "global": {
                "ecs": ecs,
                "lambda": lambda_,
            },
        }

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ecs.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    ecs_ds = xarray.open_dataset(result_dir / "work" / "ecs" / "calculate" / "ecs.nc")
    ecs = float(fillvalues_to_nan(ecs_ds["ecs"].values)[0])
    lambda_ds = xarray.open_dataset(result_dir / "work" / "ecs" / "calculate" / "lambda.nc")
    lambda_ = float(fillvalues_to_nan(lambda_ds["lambda"].values)[0])

    # Update the diagnostic bundle arguments with the computed diagnostics.
    metric_args[MetricCV.DIMENSIONS.value] = {
        MetricCV.JSON_STRUCTURE.value: [
            "region",
            "metric",
        ],
        "region": {"global": {}},
        "metric": {"ecs": {}, "lambda": {}},
    }
    metric_args[MetricCV.RESULTS.value] = {
        "global": {
            "ecs": ecs,
            "lambda": lambda_,
        },
    }

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ecs.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Only run the diagnostic that computes ECS for a single model.
    recipe["diagnostics"] = {
        "ecs": {
            "description": "Calculate ECS.",
            "variables": {
                "tas": {
                    "preprocessor": "spatial_mean",
                },
                "rtnt": {
                    "preprocessor": "spatial_mean",
                    "derive": True,
                },
            },
            "scripts": {
                "calculate": {
                    "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.
    cmip_source = get_cmip_source_type(input_files)
    if cmip_source == SourceDatasetType.CMIP6:
        df = input_files[SourceDatasetType.CMIP6]
        recipe["datasets"] = get_child_and_parent_dataset(
            df[df.variable_id == "tas"],
            parent_experiment="piControl",
            child_duration_in_years=150,
            parent_offset_in_years=0,
            parent_duration_in_years=150,
        )
    else:
        # CMIP7: use per-variable additional_datasets to preserve correct branding_suffix
        recipe_variables = dataframe_to_recipe(
            input_files[cmip_source],
            equalize_timerange=True,
        )
        recipe["datasets"] = []
        for var_name, var_settings in recipe["diagnostics"]["ecs"]["variables"].items():
            short_name = var_settings.get("short_name", var_name)
            if short_name in recipe_variables:
                var_settings["additional_datasets"] = recipe_variables[short_name]["additional_datasets"]
            elif var_name == "rtnt":
                # rtnt is derived from rlut, rsdt, rsut - use rlut's dataset
                var_settings["additional_datasets"] = recipe_variables["rlut"]["additional_datasets"]

    # Remove keys from the recipe that are only used for YAML anchors
    keys_to_remove = [
        "CMIP5_RTMT",
        "CMIP6_RTMT",
        "CMIP5_RTNT",
        "CMIP6_RTNT",
        "ECS_SCRIPT",
        "SCATTERPLOT",
    ]
    for key in keys_to_remove:
        recipe.pop(key, None)

GlobalMeanTimeseries #

Bases: ESMValToolDiagnostic

Calculate the annual mean global mean timeseries for a dataset.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/example.py
class GlobalMeanTimeseries(ESMValToolDiagnostic):
    """
    Calculate the annual mean global mean timeseries for a dataset.
    """

    name = "Global Mean Timeseries"
    slug = "global-mean-timeseries"
    base_recipe = "examples/recipe_python.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(FacetFilter(facets={"variable_id": ("tas",)}),),
                group_by=("source_id", "experiment_id", "member_id", "table_id", "variable_id", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": "tas_tavg-h2m-hxy-u",
                            "region": "glb",
                        }
                    ),
                ),
                group_by=(
                    "source_id",
                    "experiment_id",
                    "variant_label",
                    "variable_id",
                    "grid_label",
                ),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
    )

    facets = ()
    series = (
        SeriesDefinition(
            file_pattern="work/timeseries/script1/*.nc",
            dimensions={
                "statistic": "annual mean",
                "variable_id": "tas",
                "region": "global",
            },
            values_name="tas",
            index_name="time",
            attributes=[],
        ),
    )
    files = (
        FileDefinition(
            file_pattern="plots/timeseries/script1/png/*.png",
            dimensions={
                "statistic": "annual mean",
                "variable_id": "tas",
                "region": "global",
            },
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "tas"],
                            "frequency": ["fx", "mon"],
                            "experiment_id": "historical",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "tas"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "experiment_id": "historical",
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, 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[get_cmip_source_type(input_files)])
        recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}
        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)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/example.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, 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[get_cmip_source_type(input_files)])
    recipe_variables = {k: v for k, v in recipe_variables.items() if k != "areacella"}
    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)

O3LatMonthMapplot #

Bases: ESMValToolDiagnostic

Calculate the ozone diagnostics - zonal mean total column ozone vs. annual cycle plot.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
class O3LatMonthMapplot(ESMValToolDiagnostic):
    """
    Calculate the ozone diagnostics - zonal mean total column ozone vs. annual cycle plot.
    """

    name = "Ozone Diagnostics"
    slug = "ozone-annual-cycle"
    base_recipe = "ref/recipe_ref_ozone.yml"

    data_requirements = toz_data_requirement
    facets = ()
    test_data_spec = toz_test_spec
    files = (
        FileDefinition(
            file_pattern="plots/lat_month_mapplot/plot/*.png",
            dimensions={"variable_id": "toz", "statistic": "zonal mean annual cycle"},
        ),
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        dataset = recipe_variables["toz"]["additional_datasets"][0]
        # set model (CMIP6) time range to 2005...2014
        dataset["timerange"] = "2005/2014"
        recipe["datasets"] = [dataset]
        diagnostic = "lat_month_mapplot"
        recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}
        recipe["diagnostics"][diagnostic]["variables"]["toz"]["timerange"] = "2005/2014"

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    dataset = recipe_variables["toz"]["additional_datasets"][0]
    # set model (CMIP6) time range to 2005...2014
    dataset["timerange"] = "2005/2014"
    recipe["datasets"] = [dataset]
    diagnostic = "lat_month_mapplot"
    recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}
    recipe["diagnostics"][diagnostic]["variables"]["toz"]["timerange"] = "2005/2014"

O3LatTimeMapplot #

Bases: ESMValToolDiagnostic

Calculate the ozone diagnostics - zonal mean total column ozone vs. time.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
class O3LatTimeMapplot(ESMValToolDiagnostic):
    """
    Calculate the ozone diagnostics - zonal mean total column ozone vs. time.
    """

    name = "Ozone Diagnostics"
    slug = "ozone-lat-time"
    base_recipe = "ref/recipe_ref_ozone.yml"

    data_requirements = toz_data_requirement
    facets = ()
    test_data_spec = toz_test_spec
    files = (
        FileDefinition(
            file_pattern="plots/lat_time_mapplot/plot/*.png",
            dimensions={"variable_id": "toz", "statistic": "zonal mean vs time"},
        ),
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        dataset = recipe_variables["toz"]["additional_datasets"][0]
        # set time range of model (CMIP6) dataset (should match observational period)
        dataset["timerange"] = "1996/2014"
        recipe["datasets"] = [dataset]
        diagnostic = "lat_time_mapplot"
        recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}
        recipe["diagnostics"][diagnostic]["variables"]["toz"]["timerange"] = "1996/2014"

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    dataset = recipe_variables["toz"]["additional_datasets"][0]
    # set time range of model (CMIP6) dataset (should match observational period)
    dataset["timerange"] = "1996/2014"
    recipe["datasets"] = [dataset]
    diagnostic = "lat_time_mapplot"
    recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}
    recipe["diagnostics"][diagnostic]["variables"]["toz"]["timerange"] = "1996/2014"

O3PolarCapTimeseriesNH #

Bases: ESMValToolDiagnostic

Calculate the ozone diagnostics - March NH polar mean (60N-80N) time series.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
class O3PolarCapTimeseriesNH(ESMValToolDiagnostic):
    """
    Calculate the ozone diagnostics - March NH polar mean (60N-80N) time series.
    """

    name = "Ozone Diagnostics"
    slug = "ozone-nh-mar"
    base_recipe = "ref/recipe_ref_ozone.yml"

    data_requirements = toz_data_requirement
    facets = ()
    test_data_spec = toz_test_spec
    files = (
        FileDefinition(
            file_pattern="plots/polar_cap_time_series_NH/plot/timeseries_toz_NH_MAR.png",
            dimensions={"variable_id": "toz", "statistic": "Northern Hemisphere March polar mean"},
        ),
    )
    # dim0=0 is the model, dim0=1 contains the observational reference data.
    series = (
        SeriesDefinition(
            file_pattern="work/polar_cap_time_series_NH/plot/timeseries_toz_NH_MAR.nc",
            sel={"dim0": 0},
            dimensions={"variable_id": "toz", "statistic": "Northern Hemisphere March polar mean"},
            values_name="toz",
            index_name="time",
            attributes=[],
        ),
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Make sure only grid cells south of 80N are considered as there are no
        # measurements north of 80N in March. Specifying 85N as northern boundary
        # in the orignal 'recipe_ref_ozone.yml' is a bug!
        recipe["preprocessors"]["create_time_series_NH"]["extract_region"]["end_latitude"] = 80
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        dataset = recipe_variables["toz"]["additional_datasets"][0]
        # set model (CMIP6) time range to 1950...2014
        dataset["timerange"] = "1950/2014"
        recipe["datasets"] = [dataset]
        diagnostic = "polar_cap_time_series_NH"
        # adjust plot title to reflect bug fix regarding northern boundary (see above)
        recipe["diagnostics"][diagnostic]["scripts"]["plot"]["plots"]["timeseries"]["pyplot_kwargs"][
            "title"
        ] = "Total Column Ozone, 60-80N, March"
        recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Make sure only grid cells south of 80N are considered as there are no
    # measurements north of 80N in March. Specifying 85N as northern boundary
    # in the orignal 'recipe_ref_ozone.yml' is a bug!
    recipe["preprocessors"]["create_time_series_NH"]["extract_region"]["end_latitude"] = 80
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    dataset = recipe_variables["toz"]["additional_datasets"][0]
    # set model (CMIP6) time range to 1950...2014
    dataset["timerange"] = "1950/2014"
    recipe["datasets"] = [dataset]
    diagnostic = "polar_cap_time_series_NH"
    # adjust plot title to reflect bug fix regarding northern boundary (see above)
    recipe["diagnostics"][diagnostic]["scripts"]["plot"]["plots"]["timeseries"]["pyplot_kwargs"][
        "title"
    ] = "Total Column Ozone, 60-80N, March"
    recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}

O3PolarCapTimeseriesSH #

Bases: ESMValToolDiagnostic

Calculate the ozone diagnostics - October SH polar mean (60S-85S) time series.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
class O3PolarCapTimeseriesSH(ESMValToolDiagnostic):
    """
    Calculate the ozone diagnostics - October SH polar mean (60S-85S) time series.
    """

    name = "Ozone Diagnostics"
    slug = "ozone-sh-oct"
    base_recipe = "ref/recipe_ref_ozone.yml"

    data_requirements = toz_data_requirement
    facets = ()
    test_data_spec = toz_test_spec
    files = (
        FileDefinition(
            file_pattern="plots/polar_cap_time_series_SH/plot/timeseries_toz_SH_Oct.png",
            dimensions={"variable_id": "toz", "statistic": "Southern Hemisphere October polar mean"},
        ),
    )
    # dim0=0 is the model, dim0=1 contains the observational reference data.
    series = (
        SeriesDefinition(
            file_pattern="work/polar_cap_time_series_SH/plot/timeseries_toz_SH_Oct.nc",
            sel={"dim0": 0},
            dimensions={"variable_id": "toz", "statistic": "Southern Hemisphere October polar mean"},
            values_name="toz",
            index_name="time",
            attributes=[],
        ),
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        dataset = recipe_variables["toz"]["additional_datasets"][0]
        # set model (CMIP6) time range to 1950...2014
        dataset["timerange"] = "1950/2014"
        recipe["datasets"] = [dataset]
        diagnostic = "polar_cap_time_series_SH"
        recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    dataset = recipe_variables["toz"]["additional_datasets"][0]
    # set model (CMIP6) time range to 1950...2014
    dataset["timerange"] = "1950/2014"
    recipe["datasets"] = [dataset]
    diagnostic = "polar_cap_time_series_SH"
    recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}

O3ZonalMeanProfiles #

Bases: ESMValToolDiagnostic

Calculate the ozone diagnostics - stratospheric zonal mean profiles.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
class O3ZonalMeanProfiles(ESMValToolDiagnostic):
    """
    Calculate the ozone diagnostics - stratospheric zonal mean profiles.
    """

    name = "Ozone Diagnostics"
    slug = "ozone-zonal"
    base_recipe = "ref/recipe_ref_ozone.yml"

    data_requirements = (
        DataRequirement(
            source_type=SourceDatasetType.CMIP6,
            filters=(
                FacetFilter(
                    facets={
                        "variable_id": "o3",
                        "experiment_id": "historical",
                        "table_id": "Amon",
                    },
                ),
            ),
            group_by=("source_id", "member_id", "grid_label"),
            constraints=(
                RequireTimerange(
                    group_by=("instance_id",),
                    start=PartialDateTime(2005, 1),
                    end=PartialDateTime(2014, 12),
                ),
                RequireContiguousTimerange(group_by=("instance_id",)),
            ),
        ),
        # TODO: Use ESACCI-OZONE (SAGE-OMPS, variable o3) from obs4MIPs once available.
    )
    facets = ()
    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "source_id": "GFDL-ESM4",
                            "variable_id": "o3",
                        },
                        remove_ensembles=True,
                        time_span=("1996", "2015"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["historical"],
                            "source_id": "GFDL-ESM4",
                            "variable_id": "o3",
                            "branded_variable": [
                                "o3_tavg-al-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
        ),
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        dataset = recipe_variables["o3"]["additional_datasets"][0]
        # set model (CMIP6) time range to 2005...2014
        dataset["timerange"] = "2005/2014"
        recipe["datasets"] = [dataset]
        diagnostic = "zonal_mean_profiles"
        # adjust plot title to actual time range
        recipe["diagnostics"][diagnostic]["scripts"]["plot"]["plots"]["zonal_mean_profile"]["pyplot_kwargs"][
            "suptitle"
        ] = "{long_name} (2005-2014 mean)"
        recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}
        recipe["diagnostics"][diagnostic]["variables"]["o3"]["timerange"] = "2005/2014"

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/ozone.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    dataset = recipe_variables["o3"]["additional_datasets"][0]
    # set model (CMIP6) time range to 2005...2014
    dataset["timerange"] = "2005/2014"
    recipe["datasets"] = [dataset]
    diagnostic = "zonal_mean_profiles"
    # adjust plot title to actual time range
    recipe["diagnostics"][diagnostic]["scripts"]["plot"]["plots"]["zonal_mean_profile"]["pyplot_kwargs"][
        "suptitle"
    ] = "{long_name} (2005-2014 mean)"
    recipe["diagnostics"] = {diagnostic: recipe["diagnostics"][diagnostic]}
    recipe["diagnostics"][diagnostic]["variables"]["o3"]["timerange"] = "2005/2014"

RegionalHistoricalAnnualCycle #

Bases: ESMValToolDiagnostic

Plot regional historical annual cycle of climate variables.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
class RegionalHistoricalAnnualCycle(ESMValToolDiagnostic):
    """
    Plot regional historical annual cycle of climate variables.
    """

    name = "Regional historical annual cycle of climate variables"
    slug = "regional-historical-annual-cycle"
    base_recipe = "ref/recipe_ref_annual_cycle_region.yml"

    variables = (
        "hus",
        "pr",
        "psl",
        "tas",
        "ua",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": variables,
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2009, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "hus_tavg-p19-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "psl_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "ua_tavg-p19-hxy-air",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2009, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.obs4MIPs,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": (
                                "psl",
                                "ua",
                            ),
                            "source_id": "ERA-5",
                            "frequency": "mon",
                        },
                    ),
                ),
                group_by=("source_id",),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2009, 12),
                    ),
                ),
                # TODO: Add obs4MIPs datasets once available and working:
                #
                # obs4MIPs dataset that cannot be ingested (https://github.com/Climate-REF/climate-ref/issues/260):
                # - GPCP-V2.3: pr
                #
                # Not yet available on obs4MIPs:
                # - ERA5: hus
                # - HadCRUT5_ground_5.0.1.0-analysis: tas
            ),
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["historical"],
                            "frequency": ["fx", "mon"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", *variables],
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["historical"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", *variables],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "hus_tavg-p19-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "psl_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "ua_tavg-p19-hxy-air",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
            TestCase(
                name="obs4mips",
                description="Test with obs4MIPs data.",
                requests=(
                    Obs4MIPsRequest(
                        slug="obs4mips",
                        facets={
                            "project": "obs4MIPs",
                            "source_id": "ERA-5",
                            "variable_id": [
                                "psl",
                                "ua",
                            ],
                        },
                        remove_ensembles=False,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
        ),
    )

    facets = ()
    files = tuple(
        FileDefinition(
            file_pattern=f"plots/{diag_name}/figures/annual_cycle_*_{_region_to_filename(region)}.png",
            dimensions={
                "region": region,
                "variable_id": var_name,
                "statistic": "mean",
            },
        )
        for var_name, diag_name in ANNUAL_CYCLE_DIAGNOSTICS
        for region in REGIONS
    )
    series = tuple(
        SeriesDefinition(
            file_pattern=f"work/{diag_name}/figures/annual_cycle_*_{_region_to_filename(region)}.nc",
            sel={"dim0": 0},  # Select the model and not the observation.
            dimensions={
                "region": region,
                "variable_id": var_name,
                "statistic": "mean",
            },
            values_name=var_name,
            index_name="month_number",
            attributes=[],
        )
        for var_name, diag_name in ANNUAL_CYCLE_DIAGNOSTICS
        for region in REGIONS
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Extra datasets that are not published as obs4MIPs.
        extra_datasets: dict[str, list[dict[str, str | int]]] = {
            "tas": [
                {
                    "dataset": "HadCRUT5",
                    "project": "OBS",
                    "tier": 2,
                    "type": "ground",
                    "version": "5.0.1.0-analysis",
                },
            ],
            "pr": [
                {
                    "dataset": "GPCP-V2.3",
                    "project": "obs4MIPs",
                },
            ],
            "hus": [
                {
                    "dataset": "ERA5",
                    "project": "native6",
                    "tier": 3,
                    "type": "reanaly",
                    "version": "v1",
                },
            ],
        }

        # Remove the unused regions alias.
        recipe.pop("regions")
        # Update the dataset.
        recipe.pop("datasets")
        recipe_variables = dataframe_to_recipe(next(iter(input_files.values())))
        source_type = next(iter(input_files.keys()))
        project = str(source_type).split(".")[1]
        for diagnostic_name, diagnostic in dict(recipe["diagnostics"]).items():
            for variable_group, variable in dict(diagnostic["variables"]).items():
                short_name = variable_group.split("_")[0]
                datasets = []
                if short_name in recipe_variables:
                    dataset = copy.deepcopy(recipe_variables[short_name]["additional_datasets"][0])
                    dataset.pop("timerange", None)
                    datasets.append(dataset)
                if project == "obs4MIPs" and short_name in extra_datasets:
                    datasets.extend(copy.deepcopy(extra_datasets[short_name]))
                if datasets:
                    variable["additional_datasets"] = datasets
                else:
                    # If no data is available for a diagnostic, skip it.
                    recipe["diagnostics"].pop(diagnostic_name)
                    continue
            for script_settings in diagnostic["scripts"].values():
                if project == "obs4MIPs":
                    label = "{dataset}"
                else:
                    label = "{dataset}_{ensemble}_{grid}"
                script_settings["plot_filename"] = f"{{plot_type}}_{{real_name}}_{label}_{{shape_id}}"
                for plot_settings in script_settings["plots"].values():
                    plot_settings["plot_kwargs"]["default"]["label"] = label

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Extra datasets that are not published as obs4MIPs.
    extra_datasets: dict[str, list[dict[str, str | int]]] = {
        "tas": [
            {
                "dataset": "HadCRUT5",
                "project": "OBS",
                "tier": 2,
                "type": "ground",
                "version": "5.0.1.0-analysis",
            },
        ],
        "pr": [
            {
                "dataset": "GPCP-V2.3",
                "project": "obs4MIPs",
            },
        ],
        "hus": [
            {
                "dataset": "ERA5",
                "project": "native6",
                "tier": 3,
                "type": "reanaly",
                "version": "v1",
            },
        ],
    }

    # Remove the unused regions alias.
    recipe.pop("regions")
    # Update the dataset.
    recipe.pop("datasets")
    recipe_variables = dataframe_to_recipe(next(iter(input_files.values())))
    source_type = next(iter(input_files.keys()))
    project = str(source_type).split(".")[1]
    for diagnostic_name, diagnostic in dict(recipe["diagnostics"]).items():
        for variable_group, variable in dict(diagnostic["variables"]).items():
            short_name = variable_group.split("_")[0]
            datasets = []
            if short_name in recipe_variables:
                dataset = copy.deepcopy(recipe_variables[short_name]["additional_datasets"][0])
                dataset.pop("timerange", None)
                datasets.append(dataset)
            if project == "obs4MIPs" and short_name in extra_datasets:
                datasets.extend(copy.deepcopy(extra_datasets[short_name]))
            if datasets:
                variable["additional_datasets"] = datasets
            else:
                # If no data is available for a diagnostic, skip it.
                recipe["diagnostics"].pop(diagnostic_name)
                continue
        for script_settings in diagnostic["scripts"].values():
            if project == "obs4MIPs":
                label = "{dataset}"
            else:
                label = "{dataset}_{ensemble}_{grid}"
            script_settings["plot_filename"] = f"{{plot_type}}_{{real_name}}_{label}_{{shape_id}}"
            for plot_settings in script_settings["plots"].values():
                plot_settings["plot_kwargs"]["default"]["label"] = label

RegionalHistoricalTimeSeries #

Bases: RegionalHistoricalAnnualCycle

Plot regional historical mean and anomaly of climate variables.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
class RegionalHistoricalTimeSeries(RegionalHistoricalAnnualCycle):
    """
    Plot regional historical mean and anomaly of climate variables.
    """

    name = "Regional historical mean and anomaly of climate variables"
    slug = "regional-historical-timeseries"
    base_recipe = "ref/recipe_ref_timeseries_region.yml"

    variables = (
        "hus",
        "pr",
        "psl",
        "tas",
        "ua",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": variables,
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "hus_tavg-p19-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "psl_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "ua_tavg-p19-hxy-air",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.obs4MIPs,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": (
                                "psl",
                                "ua",
                            ),
                            "source_id": "ERA-5",
                            "frequency": "mon",
                        },
                    ),
                ),
                group_by=("source_id",),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                ),
                # TODO: Add obs4MIPs datasets once available and working:
                #
                # obs4MIPs dataset that cannot be ingested (https://github.com/Climate-REF/climate-ref/issues/260):
                # - GPCP-V2.3: pr
                #
                # Not yet available on obs4MIPs:
                # - ERA5: hus
                # - HadCRUT5_ground_5.0.1.0-analysis: tas
            ),
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["historical"],
                            "frequency": ["fx", "mon"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", *variables],
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["historical"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", *variables],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "hus_tavg-p19-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "psl_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "ua_tavg-p19-hxy-air",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="obs4mips",
                description="Test with obs4MIPs data.",
                requests=(
                    Obs4MIPsRequest(
                        slug="obs4mips",
                        facets={
                            "project": "obs4MIPs",
                            "source_id": "ERA-5",
                            "variable_id": [
                                "psl",
                                "ua",
                            ],
                        },
                        remove_ensembles=False,
                        time_span=("1980", "2014"),
                    ),
                ),
            ),
        ),
    )

    files = tuple(
        FileDefinition(
            file_pattern=(f"plots/{diag_name}/figures/timeseries_*_{_region_to_filename(region)}.png"),
            dimensions={
                "region": region,
                "variable_id": var_name,
                "statistic": "mean",
            },
        )
        for var_name, diag_name in TIMESERIES_DIAGNOSTICS
        for region in REGIONS
    ) + tuple(
        FileDefinition(
            file_pattern=(
                f"plots/{diag_name}_anomalies/figures/timeseries_*_{_region_to_filename(region)}.png"
            ),
            dimensions={
                "region": region,
                "variable_id": var_name,
                "statistic": "mean anomaly",
            },
        )
        for var_name, diag_name in TIMESERIES_DIAGNOSTICS
        for region in REGIONS
    )
    series = tuple(
        SeriesDefinition(
            file_pattern=(f"work/{diag_name}/figures/timeseries_*_{_region_to_filename(region)}.nc"),
            sel={"dim0": 0},
            dimensions={
                "region": region,
                "variable_id": var_name,
                "statistic": "mean",
            },
            values_name=var_name,
            index_name="time",
            attributes=[],
        )
        for var_name, diag_name in TIMESERIES_DIAGNOSTICS
        for region in REGIONS
    ) + tuple(
        SeriesDefinition(
            file_pattern=(
                f"work/{diag_name}_anomalies/figures/timeseries_*_{_region_to_filename(region)}.nc"
            ),
            sel={"dim0": 0},
            dimensions={
                "region": region,
                "variable_id": var_name,
                "statistic": "mean anomaly",
            },
            values_name=var_name,
            index_name="time",
            attributes=[],
        )
        for var_name, diag_name in TIMESERIES_DIAGNOSTICS
        for region in REGIONS
    )

RegionalHistoricalTrend #

Bases: ESMValToolDiagnostic

Plot regional historical trend of climate variables.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
class RegionalHistoricalTrend(ESMValToolDiagnostic):
    """
    Plot regional historical trend of climate variables.
    """

    name = "Regional historical trend of climate variables"
    slug = "regional-historical-trend"
    base_recipe = "ref/recipe_ref_trend_regions.yml"

    variables = (
        "hus",
        "pr",
        "psl",
        "tas",
        "ua",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": variables,
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2009, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "hus_tavg-p19-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "psl_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "ua_tavg-p19-hxy-air",
                            ),
                            "experiment_id": "historical",
                            "frequency": "mon",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2009, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.obs4MIPs,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": (
                                "psl",
                                "tas",
                                "ua",
                            ),
                            "source_id": "ERA-5",
                            "frequency": "mon",
                        },
                    ),
                ),
                group_by=("source_id",),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1980, 1),
                        end=PartialDateTime(2009, 12),
                    ),
                ),
                # TODO: Add obs4MIPs datasets once available and working:
                #
                # obs4MIPs dataset that cannot be ingested (https://github.com/Climate-REF/climate-ref/issues/260):
                # - GPCP-V2.3: pr
                #
                # Not yet available on obs4MIPs:
                # - ERA5: hus, pr
                # - HadCRUT5_ground_5.0.1.0-analysis: tas
            ),
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["historical"],
                            "frequency": ["fx", "mon"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", *variables],
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["historical"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", *variables],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "hus_tavg-p19-hxy-u",
                                "pr_tavg-u-hxy-u",
                                "psl_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                                "ua_tavg-p19-hxy-air",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
            TestCase(
                name="obs4mips",
                description="Test with obs4MIPs data.",
                requests=(
                    Obs4MIPsRequest(
                        slug="obs4mips",
                        facets={
                            "project": "obs4MIPs",
                            "source_id": "ERA-5",
                            "variable_id": [
                                "psl",
                                "tas",
                                "ua",
                            ],
                        },
                        remove_ensembles=False,
                        time_span=("1980", "2009"),
                    ),
                ),
            ),
        ),
    )

    facets = ("grid_label", "member_id", "variant_label", "source_id", "variable_id", "region", "metric")
    files = tuple(
        FileDefinition(
            file_pattern=f"plots/{var_name}_trends/plot/seaborn_barplot.png",
            dimensions={
                "variable_id": var_name,
                "statistic": "trend",
            },
        )
        for var_name in ("hus200", "pr", "psl", "tas", "ua200")
    ) + tuple(
        FileDefinition(
            file_pattern=f"work/{var_name}_trends/plot/seaborn_barplot.nc",
            dimensions={
                "variable_id": var_name,
                "statistic": "trend",
            },
        )
        for var_name in ("hus200", "pr", "psl", "tas", "ua200")
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Extra datasets that are not published as CMIP6-style obs4MIPs.
        extra_datasets: dict[str, list[dict[str, str | int]]] = {
            "tas": [
                {
                    "dataset": "HadCRUT5",
                    "project": "OBS",
                    "tier": 2,
                    "type": "ground",
                    "version": "5.0.1.0-analysis",
                },
            ],
            "pr": [
                {
                    "dataset": "GPCP-V2.3",
                    "project": "obs4MIPs",
                },
                {
                    "dataset": "ERA5",
                    "project": "native6",
                    "type": "reanaly",
                    "tier": 3,
                    "version": "v1",
                },
            ],
            "hus": [
                {
                    "dataset": "ERA5",
                    "project": "native6",
                    "tier": 3,
                    "type": "reanaly",
                    "version": "v1",
                },
            ],
        }

        # Update the datasets.
        recipe.pop("datasets")
        recipe_variables = dataframe_to_recipe(next(iter(input_files.values())))
        source_type = next(iter(input_files.keys()))
        project = str(source_type).split(".")[1]
        for diagnostic_name, diagnostic in dict(recipe["diagnostics"]).items():
            for short_name, variable in dict(diagnostic["variables"]).items():
                datasets = []
                if short_name in recipe_variables:
                    dataset = recipe_variables[short_name]["additional_datasets"][0]
                    dataset.pop("timerange", None)
                    datasets.append(dataset)
                if project == "obs4MIPs" and short_name in extra_datasets:
                    datasets.extend(extra_datasets[short_name])
                if datasets:
                    variable["additional_datasets"] = datasets
                else:
                    # If no data is available for a diagnostic, skip it.
                    recipe["diagnostics"].pop(diagnostic_name)

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": ["variable_id", "region", "metric"],
            "variable_id": {},
            "region": {},
            "metric": {"trend": {}},
        }
        for file in result_dir.glob("work/*_trends/plot/seaborn_barplot.nc"):
            ds = xarray.open_dataset(file)
            dataset_collection = next(iter(execution_dataset.values()))
            for source_id in dataset_collection.source_id.unique():
                select = source_id == np.array([s.strip() for s in ds.dataset.values.astype(str).tolist()])
                ds.isel(dim0=select)
                variable_id = next(iter(ds.data_vars.keys()))
                metric_args[MetricCV.DIMENSIONS.value]["variable_id"][variable_id] = {}
                metric_args[MetricCV.RESULTS.value][variable_id] = {}
                for region_value, trend_value in zip(
                    ds.shape_id.astype(str).values, fillvalues_to_nan(ds[variable_id].values)
                ):
                    region = region_value.strip()
                    trend = float(trend_value)
                    if region not in metric_args[MetricCV.DIMENSIONS.value]["region"]:
                        metric_args[MetricCV.DIMENSIONS.value]["region"][region] = {}
                    metric_args[MetricCV.RESULTS.value][variable_id][region] = {"trend": trend}

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": ["variable_id", "region", "metric"],
        "variable_id": {},
        "region": {},
        "metric": {"trend": {}},
    }
    for file in result_dir.glob("work/*_trends/plot/seaborn_barplot.nc"):
        ds = xarray.open_dataset(file)
        dataset_collection = next(iter(execution_dataset.values()))
        for source_id in dataset_collection.source_id.unique():
            select = source_id == np.array([s.strip() for s in ds.dataset.values.astype(str).tolist()])
            ds.isel(dim0=select)
            variable_id = next(iter(ds.data_vars.keys()))
            metric_args[MetricCV.DIMENSIONS.value]["variable_id"][variable_id] = {}
            metric_args[MetricCV.RESULTS.value][variable_id] = {}
            for region_value, trend_value in zip(
                ds.shape_id.astype(str).values, fillvalues_to_nan(ds[variable_id].values)
            ):
                region = region_value.strip()
                trend = float(trend_value)
                if region not in metric_args[MetricCV.DIMENSIONS.value]["region"]:
                    metric_args[MetricCV.DIMENSIONS.value]["region"][region] = {}
                metric_args[MetricCV.RESULTS.value][variable_id][region] = {"trend": trend}

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/regional_historical_changes.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Extra datasets that are not published as CMIP6-style obs4MIPs.
    extra_datasets: dict[str, list[dict[str, str | int]]] = {
        "tas": [
            {
                "dataset": "HadCRUT5",
                "project": "OBS",
                "tier": 2,
                "type": "ground",
                "version": "5.0.1.0-analysis",
            },
        ],
        "pr": [
            {
                "dataset": "GPCP-V2.3",
                "project": "obs4MIPs",
            },
            {
                "dataset": "ERA5",
                "project": "native6",
                "type": "reanaly",
                "tier": 3,
                "version": "v1",
            },
        ],
        "hus": [
            {
                "dataset": "ERA5",
                "project": "native6",
                "tier": 3,
                "type": "reanaly",
                "version": "v1",
            },
        ],
    }

    # Update the datasets.
    recipe.pop("datasets")
    recipe_variables = dataframe_to_recipe(next(iter(input_files.values())))
    source_type = next(iter(input_files.keys()))
    project = str(source_type).split(".")[1]
    for diagnostic_name, diagnostic in dict(recipe["diagnostics"]).items():
        for short_name, variable in dict(diagnostic["variables"]).items():
            datasets = []
            if short_name in recipe_variables:
                dataset = recipe_variables[short_name]["additional_datasets"][0]
                dataset.pop("timerange", None)
                datasets.append(dataset)
            if project == "obs4MIPs" and short_name in extra_datasets:
                datasets.extend(extra_datasets[short_name])
            if datasets:
                variable["additional_datasets"] = datasets
            else:
                # If no data is available for a diagnostic, skip it.
                recipe["diagnostics"].pop(diagnostic_name)

SeaIceAreaBasic #

Bases: ESMValToolDiagnostic

Calculate seasonal cycle and time series of NH and SH sea ice area.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/sea_ice_area_basic.py
class SeaIceAreaBasic(ESMValToolDiagnostic):
    """
    Calculate seasonal cycle and time series of NH and SH sea ice area.
    """

    name = "Sea ice area basic"
    slug = "sea-ice-area-basic"
    base_recipe = "ref/recipe_ref_sea_ice_area_basic.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": "siconc",
                            "experiment_id": "historical",
                            "table_id": "SImon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1979, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP6),
                    RequireFacets("variable_id", ("siconc", "areacello")),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": "siconc_tavg-u-hxy-u",
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1979, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP7),
                    RequireFacets("variable_id", ("siconc", "areacello")),
                ),
            ),
        ),
        # TODO: Use OSI-450-nh and OSI-450-sh from obs4MIPs once available.
    )
    facets = ()
    files = tuple(
        FileDefinition(
            file_pattern=f"plots/siarea_min/allplots/timeseries_sea_ice_area_{region}_*.png",
            dimensions={
                "region": REGIONS[region],
                "statistic": f"{MONTHS[region]} sea ice area",
            },
        )
        for region in REGIONS
    ) + tuple(
        FileDefinition(
            file_pattern=f"plots/siarea_seas/allplots/annual_cycle_sea_ice_area_{region}_*.png",
            dimensions={
                "region": REGIONS[region],
                "statistic": "20-year average seasonal cycle of the sea ice area",
            },
        )
        for region in REGIONS
    )
    series = tuple(
        SeriesDefinition(
            file_pattern=f"siarea_min/allplots/timeseries_sea_ice_area_{region}_*.nc",
            sel={"dim0": i},
            dimensions=(
                {
                    "region": REGIONS[region],
                    "statistic": f"{MONTHS[region]} sea ice area",
                }
                | ({} if i == 0 else {"reference_source_id": f"OSI-450-{region}"})
            ),
            values_name="siconc",
            index_name="time",
            attributes=[],
        )
        for region in REGIONS
        for i in range(2)
    ) + tuple(
        SeriesDefinition(
            file_pattern=f"siarea_seas/allplots/annual_cycle_sea_ice_area_{region}_*.nc",
            sel={"dim0": i},
            dimensions=(
                {
                    "region": REGIONS[region],
                    "statistic": "20-year average seasonal cycle of the sea ice area",
                }
                | ({} if i == 0 else {"reference_source_id": f"OSI-450-{region}"})
            ),
            values_name="siconc",
            index_name="month_number",
            attributes=[],
        )
        for region in REGIONS
        for i in range(2)
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacello", "siconc"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1979", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacello", "siconc"],
                            "branded_variable": [
                                "areacello_ti-u-hxy-u",
                                "siconc_tavg-u-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1979", "2014"),
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Update datasets
        recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
        recipe["datasets"] = recipe_variables["siconc"]["additional_datasets"]

        # Use the timerange from the recipe, as defined in the variable.
        for dataset in recipe["datasets"]:
            dataset.pop("timerange")

        # Update observational datasets
        nh_obs = {
            "dataset": "OSI-450-nh",
            "mip": "OImon",
            "project": "OBS",
            "supplementary_variables": [
                {
                    "short_name": "areacello",
                    "mip": "fx",
                },
            ],
            "tier": 2,
            "type": "reanaly",
            "version": "v3",
        }
        sh_obs = nh_obs.copy()
        sh_obs["dataset"] = "OSI-450-sh"
        diagnostics = recipe["diagnostics"]
        diagnostics["siarea_min"]["variables"]["sea_ice_area_nh_sep"]["additional_datasets"] = [nh_obs]
        diagnostics["siarea_min"]["variables"]["sea_ice_area_sh_feb"]["additional_datasets"] = [sh_obs]
        diagnostics["siarea_seas"]["variables"]["sea_ice_area_nh"]["additional_datasets"] = [nh_obs]
        diagnostics["siarea_seas"]["variables"]["sea_ice_area_sh"]["additional_datasets"] = [sh_obs]

        # Update the captions.
        dataset = "{dataset}.{ensemble}.{grid}".format(**recipe["datasets"][0])
        for diagnostic in diagnostics.values():
            for script_settings in diagnostic["scripts"].values():
                for plot_settings in script_settings["plots"].values():
                    plot_settings["caption"] = plot_settings["caption"].replace("[dataset]", dataset)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/sea_ice_area_basic.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Update datasets
    recipe_variables = dataframe_to_recipe(input_files[get_cmip_source_type(input_files)])
    recipe["datasets"] = recipe_variables["siconc"]["additional_datasets"]

    # Use the timerange from the recipe, as defined in the variable.
    for dataset in recipe["datasets"]:
        dataset.pop("timerange")

    # Update observational datasets
    nh_obs = {
        "dataset": "OSI-450-nh",
        "mip": "OImon",
        "project": "OBS",
        "supplementary_variables": [
            {
                "short_name": "areacello",
                "mip": "fx",
            },
        ],
        "tier": 2,
        "type": "reanaly",
        "version": "v3",
    }
    sh_obs = nh_obs.copy()
    sh_obs["dataset"] = "OSI-450-sh"
    diagnostics = recipe["diagnostics"]
    diagnostics["siarea_min"]["variables"]["sea_ice_area_nh_sep"]["additional_datasets"] = [nh_obs]
    diagnostics["siarea_min"]["variables"]["sea_ice_area_sh_feb"]["additional_datasets"] = [sh_obs]
    diagnostics["siarea_seas"]["variables"]["sea_ice_area_nh"]["additional_datasets"] = [nh_obs]
    diagnostics["siarea_seas"]["variables"]["sea_ice_area_sh"]["additional_datasets"] = [sh_obs]

    # Update the captions.
    dataset = "{dataset}.{ensemble}.{grid}".format(**recipe["datasets"][0])
    for diagnostic in diagnostics.values():
        for script_settings in diagnostic["scripts"].values():
            for plot_settings in script_settings["plots"].values():
                plot_settings["caption"] = plot_settings["caption"].replace("[dataset]", dataset)

SeaIceSensitivity #

Bases: ESMValToolDiagnostic

Calculate sea ice sensitivity.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/sea_ice_sensitivity.py
class SeaIceSensitivity(ESMValToolDiagnostic):
    """
    Calculate sea ice sensitivity.
    """

    name = "Sea ice sensitivity"
    slug = "sea-ice-sensitivity"
    base_recipe = "recipe_seaice_sensitivity.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": "siconc",
                            "experiment_id": "historical",
                            "table_id": "SImon",
                        },
                    ),
                    FacetFilter(
                        facets={
                            "variable_id": "tas",
                            "experiment_id": "historical",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("experiment_id",),  # this does nothing, but group_by cannot be empty
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1979, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    RequireFacets(
                        "variable_id",
                        required_facets=("siconc", "tas"),
                        group_by=("source_id", "member_id", "grid_label"),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                    AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP6),
                    RequireFacets(
                        "variable_id",
                        required_facets=("areacello",),
                        group_by=("source_id", "grid_label"),
                    ),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": "siconc_tavg-u-hxy-u",
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                    FacetFilter(
                        facets={
                            "branded_variable": "tas_tavg-h2m-hxy-u",
                            "experiment_id": "historical",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("experiment_id",),  # this does nothing, but group_by cannot be empty
                constraints=(
                    RequireTimerange(
                        group_by=("instance_id",),
                        start=PartialDateTime(1979, 1),
                        end=PartialDateTime(2014, 12),
                    ),
                    RequireFacets(
                        "variable_id",
                        required_facets=("siconc", "tas"),
                        group_by=("source_id", "variant_label", "grid_label"),
                    ),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                    AddSupplementaryDataset.from_defaults("areacello", SourceDatasetType.CMIP7),
                    RequireFacets(
                        "variable_id",
                        required_facets=("areacello",),
                        group_by=("source_id", "grid_label"),
                    ),
                ),
            ),
        ),
    )
    facets = ("experiment_id", "source_id", "region", "metric")
    files = tuple(
        FileDefinition(
            file_pattern=f"plots/{region}/sea_ice_sensitivity_script/png/*.png",
            dimensions={"region": region},
        )
        for region in ("arctic", "antarctic")
    ) + tuple(
        FileDefinition(
            file_pattern=f"work/{region}/sea_ice_sensitivity_script/plotted_values.csv",
            dimensions={"region": region},
        )
        for region in ("arctic", "antarctic")
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "areacello", "siconc", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                        time_span=("1979", "2014"),
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": "historical",
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "areacello", "siconc", "tas"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "areacello_ti-u-hxy-u",
                                "siconc_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                        time_span=("1979", "2014"),
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        cmip_source = get_cmip_source_type(input_files)
        recipe_variables = dataframe_to_recipe(input_files[cmip_source])

        if cmip_source == SourceDatasetType.CMIP7:
            # CMIP7: use per-variable additional_datasets to preserve correct branding_suffix
            recipe["datasets"] = []
            for diagnostic in recipe["diagnostics"].values():
                for var_name, variable in diagnostic.get("variables", {}).items():
                    short_name = variable.get("short_name", var_name)
                    if short_name in recipe_variables:
                        datasets = recipe_variables[short_name]["additional_datasets"]
                        for ds in datasets:
                            ds.pop("mip", None)
                            ds["timerange"] = "1979/2014"
                        variable["additional_datasets"] = datasets
        else:
            datasets = recipe_variables["tas"]["additional_datasets"]
            for dataset in datasets:
                dataset.pop("mip")
                dataset["timerange"] = "1979/2014"
            recipe["datasets"] = datasets

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": [
                "source_id",
                "region",
                "metric",
            ],
            "source_id": {},
            "region": {},
            "metric": {},
        }
        for region in "antarctic", "arctic":
            df = pd.read_csv(
                result_dir / "work" / region / "sea_ice_sensitivity_script" / "plotted_values.csv"
            )
            df = df.rename(columns={"Unnamed: 0": "source_id"}).drop(columns=["label"])
            metric_args[MetricCV.DIMENSIONS.value]["region"][region] = {}
            for metric in df.columns[1:]:
                metric_args[MetricCV.DIMENSIONS.value]["metric"][metric] = {}
            for row in df.itertuples(index=False):
                source_id = row.source_id
                metric_args[MetricCV.DIMENSIONS.value]["source_id"][source_id] = {}
                for metric, value in zip(df.columns[1:], row[1:]):
                    if source_id not in metric_args[MetricCV.RESULTS.value]:
                        metric_args[MetricCV.RESULTS.value][source_id] = {}
                    if region not in metric_args[MetricCV.RESULTS.value][source_id]:
                        metric_args[MetricCV.RESULTS.value][source_id][region] = {}
                    metric_args[MetricCV.RESULTS.value][source_id][region][metric] = value

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/sea_ice_sensitivity.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": [
            "source_id",
            "region",
            "metric",
        ],
        "source_id": {},
        "region": {},
        "metric": {},
    }
    for region in "antarctic", "arctic":
        df = pd.read_csv(
            result_dir / "work" / region / "sea_ice_sensitivity_script" / "plotted_values.csv"
        )
        df = df.rename(columns={"Unnamed: 0": "source_id"}).drop(columns=["label"])
        metric_args[MetricCV.DIMENSIONS.value]["region"][region] = {}
        for metric in df.columns[1:]:
            metric_args[MetricCV.DIMENSIONS.value]["metric"][metric] = {}
        for row in df.itertuples(index=False):
            source_id = row.source_id
            metric_args[MetricCV.DIMENSIONS.value]["source_id"][source_id] = {}
            for metric, value in zip(df.columns[1:], row[1:]):
                if source_id not in metric_args[MetricCV.RESULTS.value]:
                    metric_args[MetricCV.RESULTS.value][source_id] = {}
                if region not in metric_args[MetricCV.RESULTS.value][source_id]:
                    metric_args[MetricCV.RESULTS.value][source_id][region] = {}
                metric_args[MetricCV.RESULTS.value][source_id][region][metric] = value

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/sea_ice_sensitivity.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    cmip_source = get_cmip_source_type(input_files)
    recipe_variables = dataframe_to_recipe(input_files[cmip_source])

    if cmip_source == SourceDatasetType.CMIP7:
        # CMIP7: use per-variable additional_datasets to preserve correct branding_suffix
        recipe["datasets"] = []
        for diagnostic in recipe["diagnostics"].values():
            for var_name, variable in diagnostic.get("variables", {}).items():
                short_name = variable.get("short_name", var_name)
                if short_name in recipe_variables:
                    datasets = recipe_variables[short_name]["additional_datasets"]
                    for ds in datasets:
                        ds.pop("mip", None)
                        ds["timerange"] = "1979/2014"
                    variable["additional_datasets"] = datasets
    else:
        datasets = recipe_variables["tas"]["additional_datasets"]
        for dataset in datasets:
            dataset.pop("mip")
            dataset["timerange"] = "1979/2014"
        recipe["datasets"] = datasets

TransientClimateResponse #

Bases: ESMValToolDiagnostic

Calculate the global mean transient climate response for a dataset.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/tcr.py
class TransientClimateResponse(ESMValToolDiagnostic):
    """
    Calculate the global mean transient climate response for a dataset.
    """

    name = "Transient Climate Response"
    slug = "transient-climate-response"
    base_recipe = "recipe_tcr.yml"

    experiments = (
        "1pctCO2",
        "piControl",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": ("tas",),
                            "experiment_id": "1pctCO2",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    AddParentDataset.from_defaults(SourceDatasetType.CMIP6),
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": "tas_tavg-h2m-hxy-u",
                            "experiment_id": experiments,
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireFacets("experiment_id", experiments),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
    )
    facets = ("grid_label", "member_id", "variant_label", "source_id", "region", "metric")
    series = (
        SeriesDefinition(
            file_pattern="tcr/calculate/{source_id}*.nc",
            dimensions={
                "statistic": "global annual mean tas anomaly relative to linear fit of piControl run",
            },
            values_name="tas_anomaly",
            index_name="time",
            attributes=[],
        ),
    )
    files = (
        FileDefinition(
            file_pattern="plots/tcr/calculate/*.png",
            dimensions={
                "statistic": "global annual mean tas anomaly relative to linear fit of piControl run",
            },
        ),
        FileDefinition(
            file_pattern="work/tcr/calculate/tcr.nc",
            dimensions={"metric": "tcr"},
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["1pctCO2", "piControl"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["1pctCO2", "piControl"],
                            "source_id": "CanESM5",
                            "variable_id": ["areacella", "tas"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Only run the diagnostic that computes TCR for a single model.
        recipe["diagnostics"] = {
            "tcr": {
                "description": "Calculate TCR.",
                "variables": {
                    "tas": {
                        "preprocessor": "spatial_mean",
                    },
                },
                "scripts": {
                    "calculate": {
                        "script": "climate_metrics/tcr.py",
                        "calculate_mmm": False,
                    },
                },
            },
        }

        # Prepare updated datasets section in recipe. It contains two
        # datasets, one for the "1pctCO2" and one for the "piControl"
        # experiment.
        cmip_source = get_cmip_source_type(input_files)
        df = input_files[cmip_source]
        recipe["datasets"] = get_child_and_parent_dataset(
            df[df.variable_id == "tas"],
            parent_experiment="piControl",
            child_duration_in_years=140,
            parent_offset_in_years=0,
            parent_duration_in_years=140,
        )

        # Remove keys from the recipe that are only used for YAML anchors
        keys_to_remove = [
            "TCR",
            "SCATTERPLOT",
            "VAR_SETTING",
        ]
        for key in keys_to_remove:
            recipe.pop(key, None)

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        tcr_ds = xarray.open_dataset(result_dir / "work" / "tcr" / "calculate" / "tcr.nc")
        tcr = float(fillvalues_to_nan(tcr_ds["tcr"].values)[0])

        # Update the diagnostic bundle arguments with the computed diagnostics.
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": [
                "region",
                "metric",
            ],
            "region": {"global": {}},
            "metric": {"tcr": {}},
        }
        metric_args[MetricCV.RESULTS.value] = {
            "global": {
                "tcr": tcr,
            },
        }

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/tcr.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    tcr_ds = xarray.open_dataset(result_dir / "work" / "tcr" / "calculate" / "tcr.nc")
    tcr = float(fillvalues_to_nan(tcr_ds["tcr"].values)[0])

    # Update the diagnostic bundle arguments with the computed diagnostics.
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": [
            "region",
            "metric",
        ],
        "region": {"global": {}},
        "metric": {"tcr": {}},
    }
    metric_args[MetricCV.RESULTS.value] = {
        "global": {
            "tcr": tcr,
        },
    }

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/tcr.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Only run the diagnostic that computes TCR for a single model.
    recipe["diagnostics"] = {
        "tcr": {
            "description": "Calculate TCR.",
            "variables": {
                "tas": {
                    "preprocessor": "spatial_mean",
                },
            },
            "scripts": {
                "calculate": {
                    "script": "climate_metrics/tcr.py",
                    "calculate_mmm": False,
                },
            },
        },
    }

    # Prepare updated datasets section in recipe. It contains two
    # datasets, one for the "1pctCO2" and one for the "piControl"
    # experiment.
    cmip_source = get_cmip_source_type(input_files)
    df = input_files[cmip_source]
    recipe["datasets"] = get_child_and_parent_dataset(
        df[df.variable_id == "tas"],
        parent_experiment="piControl",
        child_duration_in_years=140,
        parent_offset_in_years=0,
        parent_duration_in_years=140,
    )

    # Remove keys from the recipe that are only used for YAML anchors
    keys_to_remove = [
        "TCR",
        "SCATTERPLOT",
        "VAR_SETTING",
    ]
    for key in keys_to_remove:
        recipe.pop(key, None)

TransientClimateResponseEmissions #

Bases: ESMValToolDiagnostic

Calculate the global mean Transient Climate Response to Cumulative CO2 Emissions.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/tcre.py
class TransientClimateResponseEmissions(ESMValToolDiagnostic):
    """
    Calculate the global mean Transient Climate Response to Cumulative CO2 Emissions.
    """

    name = "Transient Climate Response to Cumulative CO2 Emissions"
    slug = "transient-climate-response-emissions"
    base_recipe = "recipe_tcre.yml"

    variables = (
        "tas",
        "fco2antt",
    )

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": "tas",
                            "experiment_id": "esm-1pctCO2",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    AddParentDataset.from_defaults(SourceDatasetType.CMIP6),
                    AddSupplementaryDataset(
                        supplementary_facets={
                            "variable_id": "fco2antt",
                            "experiment_id": "esm-1pctCO2",
                        },
                        matching_facets=(
                            "source_id",
                            "member_id",
                            "table_id",
                            "grid_label",
                        ),
                        optional_matching_facets=("version",),
                    ),
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireFacets("variable_id", ("tas", "fco2antt")),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": (
                                "fco2antt_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ),
                            "experiment_id": "esm-1pctCO2",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                    FacetFilter(
                        facets={
                            "branded_variable": "tas_tavg-h2m-hxy-u",
                            "experiment_id": "esm-piControl",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    RequireFacets("experiment_id", ("esm-1pctCO2", "esm-piControl")),
                    RequireFacets("variable_id", variables),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
    )
    facets = ("grid_label", "member_id", "variant_label", "source_id", "region", "metric")
    # TODO: the ESMValTool diagnostic script does not save the data for the timeseries.
    series = tuple()
    files = (
        FileDefinition(
            file_pattern="plots/tcre/calculate_tcre/*.png",
            dimensions={"statistic": "tcre"},
        ),
        FileDefinition(
            file_pattern="work/tcre/calculate_tcre/tcre.nc",
            dimensions={"metric": "tcre"},
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["esm-1pctCO2", "esm-piControl"],
                            "source_id": "MPI-ESM1-2-LR",
                            "variable_id": ["areacella", "fco2antt", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["esm-1pctCO2", "esm-piControl"],
                            "source_id": "MPI-ESM1-2-LR",
                            "variable_id": ["areacella", "fco2antt", "tas"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "fco2antt_tavg-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Prepare updated datasets section in recipe. It contains three
        # datasets, "tas" and "fco2antt" for the "esm-1pctCO2" and just "tas"
        # for the "esm-piControl" experiment.
        cmip_source = get_cmip_source_type(input_files)
        df = input_files[cmip_source]
        tas_esm_1pctCO2, tas_esm_piControl = get_child_and_parent_dataset(
            df[df.variable_id == "tas"],
            parent_experiment="esm-piControl",
            child_duration_in_years=65,
            parent_offset_in_years=0,
            parent_duration_in_years=65,
        )
        recipe_variables = dataframe_to_recipe(df[df.variable_id == "fco2antt"])

        fco2antt_esm_1pctCO2 = next(
            ds for ds in recipe_variables["fco2antt"]["additional_datasets"] if ds["exp"] == "esm-1pctCO2"
        )
        fco2antt_esm_1pctCO2["timerange"] = tas_esm_1pctCO2["timerange"]

        recipe["diagnostics"]["tcre"]["variables"] = {
            "tas_esm-1pctCO2": {
                "short_name": "tas",
                "preprocessor": "global_annual_mean_anomaly",
                "additional_datasets": [tas_esm_1pctCO2],
            },
            "tas_esm-piControl": {
                "short_name": "tas",
                "preprocessor": "global_annual_mean_anomaly",
                "additional_datasets": [tas_esm_piControl],
            },
            "fco2antt": {
                "preprocessor": "global_cumulative_sum",
                "additional_datasets": [fco2antt_esm_1pctCO2],
            },
        }
        recipe["diagnostics"].pop("barplot")

        # Update descriptions.
        dataset = tas_esm_1pctCO2["dataset"]
        ensemble = tas_esm_1pctCO2["ensemble"]
        settings = recipe["diagnostics"]["tcre"]["scripts"]["calculate_tcre"]
        settings["caption"] = (
            settings["caption"].replace("MPI-ESM1-2-LR", dataset).replace("r1i1p1f1", ensemble)
        )
        settings["pyplot_kwargs"]["title"] = (
            settings["pyplot_kwargs"]["title"].replace("MPI-ESM1-2-LR", dataset).replace("r1i1p1f1", ensemble)
        )

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        tcre_ds = xarray.open_dataset(result_dir / "work" / "tcre" / "calculate_tcre" / "tcre.nc")
        tcre = float(fillvalues_to_nan(tcre_ds["tcre"].values)[0])

        # Update the diagnostic bundle arguments with the computed diagnostics.
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": [
                "region",
                "metric",
            ],
            "region": {"global": {}},
            "metric": {"tcre": {}},
        }
        metric_args[MetricCV.RESULTS.value] = {
            "global": {
                "tcre": tcre,
            },
        }
        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/tcre.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    tcre_ds = xarray.open_dataset(result_dir / "work" / "tcre" / "calculate_tcre" / "tcre.nc")
    tcre = float(fillvalues_to_nan(tcre_ds["tcre"].values)[0])

    # Update the diagnostic bundle arguments with the computed diagnostics.
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": [
            "region",
            "metric",
        ],
        "region": {"global": {}},
        "metric": {"tcre": {}},
    }
    metric_args[MetricCV.RESULTS.value] = {
        "global": {
            "tcre": tcre,
        },
    }
    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/tcre.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Prepare updated datasets section in recipe. It contains three
    # datasets, "tas" and "fco2antt" for the "esm-1pctCO2" and just "tas"
    # for the "esm-piControl" experiment.
    cmip_source = get_cmip_source_type(input_files)
    df = input_files[cmip_source]
    tas_esm_1pctCO2, tas_esm_piControl = get_child_and_parent_dataset(
        df[df.variable_id == "tas"],
        parent_experiment="esm-piControl",
        child_duration_in_years=65,
        parent_offset_in_years=0,
        parent_duration_in_years=65,
    )
    recipe_variables = dataframe_to_recipe(df[df.variable_id == "fco2antt"])

    fco2antt_esm_1pctCO2 = next(
        ds for ds in recipe_variables["fco2antt"]["additional_datasets"] if ds["exp"] == "esm-1pctCO2"
    )
    fco2antt_esm_1pctCO2["timerange"] = tas_esm_1pctCO2["timerange"]

    recipe["diagnostics"]["tcre"]["variables"] = {
        "tas_esm-1pctCO2": {
            "short_name": "tas",
            "preprocessor": "global_annual_mean_anomaly",
            "additional_datasets": [tas_esm_1pctCO2],
        },
        "tas_esm-piControl": {
            "short_name": "tas",
            "preprocessor": "global_annual_mean_anomaly",
            "additional_datasets": [tas_esm_piControl],
        },
        "fco2antt": {
            "preprocessor": "global_cumulative_sum",
            "additional_datasets": [fco2antt_esm_1pctCO2],
        },
    }
    recipe["diagnostics"].pop("barplot")

    # Update descriptions.
    dataset = tas_esm_1pctCO2["dataset"]
    ensemble = tas_esm_1pctCO2["ensemble"]
    settings = recipe["diagnostics"]["tcre"]["scripts"]["calculate_tcre"]
    settings["caption"] = (
        settings["caption"].replace("MPI-ESM1-2-LR", dataset).replace("r1i1p1f1", ensemble)
    )
    settings["pyplot_kwargs"]["title"] = (
        settings["pyplot_kwargs"]["title"].replace("MPI-ESM1-2-LR", dataset).replace("r1i1p1f1", ensemble)
    )

ZeroEmissionCommitment #

Bases: ESMValToolDiagnostic

Calculate the global mean Zero Emission Commitment (ZEC) temperature.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/zec.py
class ZeroEmissionCommitment(ESMValToolDiagnostic):
    """
    Calculate the global mean Zero Emission Commitment (ZEC) temperature.
    """

    name = "Zero Emission Commitment"
    slug = "zero-emission-commitment"
    base_recipe = "recipe_zec.yml"

    data_requirements = (
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP6,
                filters=(
                    FacetFilter(
                        facets={
                            "variable_id": "tas",
                            "experiment_id": "esm-1pct-brch-1000PgC",
                            "table_id": "Amon",
                        },
                    ),
                ),
                group_by=("source_id", "member_id", "grid_label"),
                constraints=(
                    AddParentDataset.from_defaults(SourceDatasetType.CMIP6),
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP6),
                ),
            ),
        ),
        (
            DataRequirement(
                source_type=SourceDatasetType.CMIP7,
                filters=(
                    FacetFilter(
                        facets={
                            "branded_variable": "tas_tavg-h2m-hxy-u",
                            "experiment_id": "esm-flat10-zec",
                            "frequency": "mon",
                            "region": "glb",
                        },
                    ),
                ),
                group_by=("source_id", "variant_label", "grid_label"),
                constraints=(
                    AddParentDataset.from_defaults(SourceDatasetType.CMIP7),
                    RequireContiguousTimerange(group_by=("instance_id",)),
                    AddSupplementaryDataset.from_defaults("areacella", SourceDatasetType.CMIP7),
                ),
            ),
        ),
    )
    facets = ("grid_label", "member_id", "variant_label", "source_id", "region", "metric")
    series = (
        SeriesDefinition(
            file_pattern="work/zec/zec/zec.nc",
            sel={"dim0": 0},
            dimensions={
                "statistic": "zec",
            },
            values_name="zec",
            index_name="time",
            attributes=[],
        ),
    )
    files = (
        FileDefinition(
            file_pattern="plots/zec/zec/*.png",
            dimensions={"statistic": "zec"},
        ),
        FileDefinition(
            file_pattern="work/zec/zec/zec_50.nc",
            dimensions={"metric": "zec"},
        ),
    )

    test_data_spec = TestDataSpecification(
        test_cases=(
            TestCase(
                name="cmip6-1pctCO2-parent",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["1pctCO2", "esm-1pct-brch-1000PgC"],
                            "source_id": "ACCESS-ESM1-5",
                            "variable_id": ["areacella", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip6-esm-1pctCO2-parent",
                description="Test with CMIP6 data.",
                requests=(
                    CMIP6Request(
                        slug="cmip6",
                        facets={
                            "experiment_id": ["esm-1pctCO2", "esm-1pct-brch-1000PgC"],
                            "source_id": "MIROC-ES2L",
                            "variable_id": ["areacella", "tas"],
                            "frequency": ["fx", "mon"],
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
            TestCase(
                name="cmip7",
                description="Test with CMIP7 data.",
                requests=(
                    CMIP7Request(
                        slug="cmip7",
                        facets={
                            "experiment_id": ["esm-flat10", "esm-flat10-zec"],
                            "source_id": "ACCESS-ESM1-5",
                            "variable_id": ["areacella", "tas"],
                            "branded_variable": [
                                "areacella_ti-u-hxy-u",
                                "tas_tavg-h2m-hxy-u",
                            ],
                            "variant_label": "r1i1p1f1",
                            "frequency": ["fx", "mon"],
                            "region": "glb",
                        },
                        remove_ensembles=True,
                    ),
                ),
            ),
        )
    )

    @staticmethod
    def update_recipe(
        recipe: Recipe,
        input_files: dict[SourceDatasetType, pandas.DataFrame],
    ) -> None:
        """Update the recipe."""
        # Prepare updated datasets section in recipe. It contains two
        # datasets, one for the child and one for the parent experiment.
        cmip_source = get_cmip_source_type(input_files)
        df = input_files[cmip_source]
        child_experiment = {
            SourceDatasetType.CMIP6: "esm-1pct-brch-1000PgC",
            SourceDatasetType.CMIP7: "esm-flat10-zec",
        }[cmip_source]
        parent_experiment = next(exp for exp in df.experiment_id.unique() if exp != child_experiment)
        child_dataset, parent_dataset = get_child_and_parent_dataset(
            df[df.variable_id == "tas"],
            parent_experiment=parent_experiment,
            child_duration_in_years=100,
            parent_offset_in_years=-10,
            parent_duration_in_years=20,
        )

        variables = recipe["diagnostics"]["zec"]["variables"]
        variables["tas_base"] = {
            "short_name": "tas",
            "preprocessor": "anomaly_base",
            "additional_datasets": [parent_dataset],
        }
        variables["tas"] = {
            "preprocessor": "spatial_mean",
            "additional_datasets": [child_dataset],
        }
        recipe["diagnostics"]["zec"]["scripts"]["zec"]["experiments"] = {
            "reference": [parent_dataset["exp"]],
            "simulation": [child_dataset["exp"]],
        }

    @staticmethod
    def format_result(
        result_dir: Path,
        execution_dataset: ExecutionDatasetCollection,
        metric_args: MetricBundleArgs,
        output_args: OutputBundleArgs,
    ) -> tuple[CMECMetric, CMECOutput]:
        """Format the result."""
        zec_ds = xarray.open_dataset(result_dir / "work" / "zec" / "zec" / "zec_50.nc")
        zec = float(fillvalues_to_nan(zec_ds["zec"].values)[0])

        # Update the diagnostic bundle arguments with the computed diagnostics.
        metric_args[MetricCV.DIMENSIONS.value] = {
            "json_structure": ["region", "metric"],
            "region": {"global": {}},
            "metric": {"zec": {}},
        }
        metric_args[MetricCV.RESULTS.value] = {
            "global": {
                "zec": zec,
            },
        }

        return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

format_result(result_dir, execution_dataset, metric_args, output_args) staticmethod #

Format the result.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/zec.py
@staticmethod
def format_result(
    result_dir: Path,
    execution_dataset: ExecutionDatasetCollection,
    metric_args: MetricBundleArgs,
    output_args: OutputBundleArgs,
) -> tuple[CMECMetric, CMECOutput]:
    """Format the result."""
    zec_ds = xarray.open_dataset(result_dir / "work" / "zec" / "zec" / "zec_50.nc")
    zec = float(fillvalues_to_nan(zec_ds["zec"].values)[0])

    # Update the diagnostic bundle arguments with the computed diagnostics.
    metric_args[MetricCV.DIMENSIONS.value] = {
        "json_structure": ["region", "metric"],
        "region": {"global": {}},
        "metric": {"zec": {}},
    }
    metric_args[MetricCV.RESULTS.value] = {
        "global": {
            "zec": zec,
        },
    }

    return CMECMetric.model_validate(metric_args), CMECOutput.model_validate(output_args)

update_recipe(recipe, input_files) staticmethod #

Update the recipe.

Source code in packages/climate-ref-esmvaltool/src/climate_ref_esmvaltool/diagnostics/zec.py
@staticmethod
def update_recipe(
    recipe: Recipe,
    input_files: dict[SourceDatasetType, pandas.DataFrame],
) -> None:
    """Update the recipe."""
    # Prepare updated datasets section in recipe. It contains two
    # datasets, one for the child and one for the parent experiment.
    cmip_source = get_cmip_source_type(input_files)
    df = input_files[cmip_source]
    child_experiment = {
        SourceDatasetType.CMIP6: "esm-1pct-brch-1000PgC",
        SourceDatasetType.CMIP7: "esm-flat10-zec",
    }[cmip_source]
    parent_experiment = next(exp for exp in df.experiment_id.unique() if exp != child_experiment)
    child_dataset, parent_dataset = get_child_and_parent_dataset(
        df[df.variable_id == "tas"],
        parent_experiment=parent_experiment,
        child_duration_in_years=100,
        parent_offset_in_years=-10,
        parent_duration_in_years=20,
    )

    variables = recipe["diagnostics"]["zec"]["variables"]
    variables["tas_base"] = {
        "short_name": "tas",
        "preprocessor": "anomaly_base",
        "additional_datasets": [parent_dataset],
    }
    variables["tas"] = {
        "preprocessor": "spatial_mean",
        "additional_datasets": [child_dataset],
    }
    recipe["diagnostics"]["zec"]["scripts"]["zec"]["experiments"] = {
        "reference": [parent_dataset["exp"]],
        "simulation": [child_dataset["exp"]],
    }

sub-packages#

Sub-package Description
base
climate_at_global_warming_levels
climate_drivers_for_fire
cloud_radiative_effects
cloud_scatterplots
ecs
enso
example
ozone
regional_historical_changes
sea_ice_area_basic
sea_ice_sensitivity
tcr
tcre
zec