Source code for hydrobricks.modules.glacier

from __future__ import annotations

from abc import abstractmethod
from typing import Any

from hydrobricks.modules.base import Module


[docs] class GlacierModule(Module): """Pluggable glacier formulation for the hydrological models. A glacier module owns everything glacier-specific so that the same formulation can be shared across models (e.g. Socont and HBV) and alternative formulations swapped in. It contributes: - the glacier-related bricks added to the model structure (``add_bricks``), - the structure keys that are glacier land covers, so the model can split a glacier-free base from a with-glacier variant (``land_cover_keys``), - the glacier-specific parameter aliases (``parameter_aliases``). The melt parameters (e.g. ``a_ice``) and the ``a_snow < a_ice`` constraint are derived generically by ``ParameterSet`` from the ``melt`` process placed on the glacier land cover, so a module only needs to expose its own extra aliases (such as the response factors of its reservoirs). Resolve a glacier module with ``GlacierModule.get_module(name_or_instance)`` and register a new one with the ``@GlacierModule.register("name")`` decorator. """ _registry: dict[str, type[GlacierModule]] = {} _category = "glacier module"
[docs] @abstractmethod def add_bricks( self, structure: dict[str, Any], glacier_names: list[str], *, melt_process: str, options: dict[str, Any], ) -> None: """Add the glacier bricks for the given glacier covers into ``structure``. Parameters ---------- structure The model structure dictionary to add the bricks to (modified in place). glacier_names The names of the glacier land covers. melt_process The ice melt process kind (e.g. ``'melt:degree_day'``), usually the model's snow melt process. options The model options (a module reads its own configuration from here, e.g. ``glacier_infinite_storage``). """
[docs] @abstractmethod def land_cover_keys(self, glacier_names: list[str]) -> set[str]: """Return the structure keys that are glacier land covers. These are excluded from the glacier-free base variant (while any shared, catchment-level bricks the module adds, such as sub-basin reservoirs, stay in the base so the sub-basin owns and shares them). """
[docs] @abstractmethod def parameter_aliases(self, glacier_names: list[str]) -> dict[str, list[str]]: """Return the glacier-specific parameter aliases (e.g. reservoir factors)."""
[docs] @GlacierModule.register("gsm") class GSM(GlacierModule): """Glacier Sub-Model (GSM), as in GSM-SOCONT (Schaefli et al., 2005). The glacierized area sends its rain + snowmelt and its ice melt to two catchment-level linear reservoirs draining directly to the outlet, bypassing the soil routine. Ice melt is suppressed while snow covers the ice (``no_melt_when_snow_cover``); the ice is treated as an infinite storage by default (``glacier_infinite_storage``). The two reservoirs are shared by all glacier covers. """ RAIN_SNOWMELT_STORAGE = "glacier_area_rain_snowmelt_storage" ICEMELT_STORAGE = "glacier_area_icemelt_storage"
[docs] def add_bricks( self, structure: dict[str, Any], glacier_names: list[str], *, melt_process: str, options: dict[str, Any], ) -> None: if not glacier_names: return infinite_storage = options.get("glacier_infinite_storage", True) for cover_name in glacier_names: structure[cover_name] = { "attach_to": "hydro_unit", "kind": "land_cover", "parameters": { "no_melt_when_snow_cover": True, "infinite_storage": infinite_storage, }, "processes": { "outflow_rain_snowmelt": { "kind": "outflow:direct", "target": self.RAIN_SNOWMELT_STORAGE, "instantaneous": True, }, "melt": { "kind": melt_process, "target": self.ICEMELT_STORAGE, "instantaneous": True, }, }, } # Catchment-level reservoirs for the glacierized-area contributions. structure[self.RAIN_SNOWMELT_STORAGE] = { "attach_to": "sub_basin", "kind": "storage", "processes": {"outflow": {"kind": "outflow:linear", "target": "outlet"}}, } structure[self.ICEMELT_STORAGE] = { "attach_to": "sub_basin", "kind": "storage", "processes": {"outflow": {"kind": "outflow:linear", "target": "outlet"}}, }
[docs] def land_cover_keys(self, glacier_names: list[str]) -> set[str]: # The sub-basin reservoirs are intentionally not listed: they stay in the # glacier-free base so the (catchment-level) sub-basin owns and shares them. return set(glacier_names)
[docs] def parameter_aliases(self, glacier_names: list[str]) -> dict[str, list[str]]: if not glacier_names: return {} return { f"{self.RAIN_SNOWMELT_STORAGE}:response_factor": ["k_snow"], f"{self.ICEMELT_STORAGE}:response_factor": ["k_ice"], }