Source code for hydroflows.methods.climate.climatology

"""Method for creating monthly climatology from gridded climate data."""

import inspect
from pathlib import Path
from typing import Literal

from pydantic import ConfigDict, model_validator

from hydroflows._typing import FileDirPath, ListOfListOfInt, ListOfStr, OutputDirPath
from hydroflows.methods.climate.grid_utils import extract_climate_projections_statistics
from hydroflows.methods.utils.io import to_netcdf
from hydroflows.workflow.method import Method
from hydroflows.workflow.method_parameters import Parameters

__all__ = ["MonthlyClimatology", "Input", "Output", "Params"]


[docs] class Input(Parameters): """Input parameters. This class represents the input data required for the :py:class:`MonthlyClimatology` method. """ region: Path """ The Path to the region vector file. """ catalog_path: FileDirPath | None """ The file path to the data catalog. Should contain the the `clim_source` data. """
[docs] class Output(Parameters): """Output parameters. This class represents the output data generated by the :py:class:`MonthlyClimatology` method. """ climatology: Path """ The path to the dataset containing the climate factors. """
[docs] class Params(Parameters): """Parameters for the :py:class:`MonthlyClimatology`. Instances of this class are used in the :py:class:`MonthlyClimatology` method to define the required settings. """ model_config = ConfigDict(extra="allow") predefined_catalogs: ListOfStr | None = None """List of predefined catalogs to use.""" clim_source: Literal["cmip6", "cmip5", "isimip3"] = "cmip6" """The source of the climate data (e.g. 'cmip6', 'cmip5', 'isimip3'). Must be one of the available sources in the catalog_path.""" members: ListOfStr = ["r1i1p1f1"] """List of member names of the climate model (e.g. r1i1p1f1). Depends on the climate source.""" model: str """Model name of the climate model (e.g. 'NOAA-GFDL_GFDL-ESM4', 'INM_INM-CM5-0'). Depends on the climate source.""" scenario: str """Scenario name of the climate model (e.g. historical, ssp245, ssp585). Depends on the climate source.""" horizon: ListOfListOfInt """The horizon of the future scenario (e.g. [[2090, 2100]]).""" output_dir: OutputDirPath = OutputDirPath("data", "climatology") """The output directory of the climatology dataset.""" @model_validator(mode="after") def check_model_extra_fields(self): """Check if the model_extra fields are in the signature of extract_climate_projections_statistics.""" if self.model_extra is not None: par = set( inspect.signature( extract_climate_projections_statistics ).parameters.keys() ) for key in self.model_extra: if key not in par: raise ValueError(f"Parameter {key} unknown, use one of {par}.") return self
[docs] class MonthlyClimatology(Method): """Create monthly climatology from climate model data. Parameters ---------- region : Path Path to the region vector file. catalog_path : Path Path to the data catalog. Should contain the the `clim_source` data. model : str Model name of the climate model (e.g. 'NOAA-GFDL_GFDL-ESM4', 'INM_INM-CM5-0'). Depends on the climate source. scenario : str Scenario name of the climate model (e.g. historical, ssp245, ssp585). Depends on the climate source. horizon : ListOfListOfInt The horizon(s) of the future scenario (e.g. [[2090, 2100]]). output_dir : Path The output directory of the climatology dataset. **params Additional parameters to pass to the MonthlyClimatology instance. See :py:class:`climatology Params <hydroflows.methods.climate.climatology.Params>`. See Also -------- :py:class:`climatology Input <hydroflows.methods.climate.climatology.Input>` :py:class:`climatology Output <hydroflows.methods.climate.climatology.Output>` :py:class:`climatology Params <hydroflows.methods.climate.climatology.Params>` """ name: str = "monthly_climatology" _test_kwargs = { "region": Path("region.geojson"), "catalog_path": "data_catalog.yml", "model": "NOAA-GFDL_GFDL-ESM4", "scenario": "ssp585", "horizon": [[2090, 2100]], "output_dir": Path("data", "input", "stats"), "pet_method": "Makkink", # test extra field } def __init__( self, region: Path, model: str, scenario: str, horizon: ListOfListOfInt, catalog_path: Path | None = None, output_dir: Path = Path("data", "climatology"), **params, ) -> None: self.input: Input = Input(region=region, catalog_path=catalog_path) if ( self.input.catalog_path is None and params.get("predefined_catalogs") is None ): params["predefined_catalogs"] = ["gcs_cmip6_data"] self.params: Params = Params( model=model, scenario=scenario, horizon=horizon, output_dir=output_dir, **params, ) # post-fix required to avoid ambiguity in snakemake pf = "_future" if self.params.scenario != "historical" else "" file_name = f"climatology_{self.params.model}_{self.params.scenario}{pf}.nc" self.output: Output = Output( climatology=self.params.output_dir / file_name, ) def _run(self) -> None: """Run the gridded climate statistics method.""" # Prepare the time horizon dictionary horizons = [[str(i) for i in item] for item in self.params.horizon] if self.params.scenario == "historical": time_horizon = {self.params.scenario: horizons[0]} else: horizon_fmt = ["-".join(item) for item in horizons] time_horizon = dict(zip(horizon_fmt, horizons)) # Prepare the data libraries data_libs = ( self.params.predefined_catalogs if self.input.catalog_path is None else [] ) if self.input.catalog_path is not None: data_libs.append(self.input.catalog_path) # Execute the function stats_ds = extract_climate_projections_statistics( self.input.region, data_libs=data_libs, clim_source=self.params.clim_source, members=self.params.members, model=self.params.model, scenario=self.params.scenario, time_horizon=time_horizon, **self.params.model_extra, ) # Save to drive to_netcdf( stats_ds, file_name=self.output.climatology.name, output_dir=self.output.climatology.parent, )