Source code for hydrobricks.models.socont

from __future__ import annotations

import logging
from typing import Any

from hydrobricks._exceptions import ConfigurationError, ModelError
from hydrobricks.models import Model

logger = logging.getLogger(__name__)


[docs] class Socont(Model): """Socont model implementation""" def __init__(self, name: str = "socont", **kwargs: Any) -> None: super().__init__(name=name, **kwargs) # Default options self.options["soil_storage_nb"] = 1 self.options["surface_runoff"] = "socont_runoff" self.options["snow_melt_process"] = "melt:degree_day" self.options["snow_ice_transformation"] = None self.options["snow_redistribution"] = None self.options["glacier_infinite_storage"] = True self.allowed_land_cover_types = ["ground", "glacier"] self._set_options(kwargs) try: self._define_structure() self._generate_structure() self._define_parameter_aliases() self._define_parameter_constraints() except RuntimeError as err: raise ModelError(f"Socont model initialization raised an exception: {err}") def _define_structure(self) -> None: """ Define the Socont model structure with all bricks and processes. Sets up the model's hydrological structure including: - Glacier processes (if glacier land cover is present) - Ground infiltration and surface runoff - Soil reservoirs (slow storage, and optionally second soil layer) - Surface runoff routing (Socont or linear storage) - ET processes The structure is stored in self.structure for later generation. Raises ------ RuntimeError If surface runoff option is not recognized. """ # Add surface-related processes for cover_type, cover_name in zip(self.land_cover_types, self.land_cover_names): if cover_type == "glacier": self.structure[cover_name] = { "attach_to": "hydro_unit", "kind": "land_cover", "parameters": { "no_melt_when_snow_cover": True, "infinite_storage": self.options["glacier_infinite_storage"], }, "processes": { "outflow_rain_snowmelt": { "kind": "outflow:direct", "target": "glacier_area_rain_snowmelt_storage", "instantaneous": True, }, "melt": { "kind": self.options["snow_melt_process"], "target": "glacier_area_icemelt_storage", "instantaneous": True, }, }, } if "glacier" in self.land_cover_types: # Basin storages for contributions from the glacierized area self.structure["glacier_area_rain_snowmelt_storage"] = { "attach_to": "sub_basin", "kind": "storage", "processes": { "outflow": {"kind": "outflow:linear", "target": "outlet"} }, } self.structure["glacier_area_icemelt_storage"] = { "attach_to": "sub_basin", "kind": "storage", "processes": { "outflow": {"kind": "outflow:linear", "target": "outlet"} }, } # Infiltration and overflow self.structure["ground"] = { "attach_to": "hydro_unit", "kind": "land_cover", "processes": { "infiltration": { "kind": "infiltration:socont", "target": "slow_reservoir", }, "runoff": {"kind": "outflow:rest_direct", "target": "surface_runoff"}, }, } # Add other bricks self.structure["slow_reservoir"] = { "attach_to": "hydro_unit", "kind": "storage", "parameters": {"capacity": 200}, "processes": { "et": {"kind": "et:socont"}, "outflow": {"kind": "outflow:linear", "target": "outlet"}, "overflow": {"kind": "overflow", "target": "outlet"}, }, } if self.options["soil_storage_nb"] == 2: logger.info("Using 2 soil storages.") self.structure["slow_reservoir"]["processes"]["percolation"] = { "kind": "percolation:constant", "target": "slow_reservoir_2", } self.structure["slow_reservoir_2"] = { "attach_to": "hydro_unit", "kind": "storage", "processes": { "outflow": {"kind": "outflow:linear", "target": "outlet"} }, } # Add surface runoff if self.options["surface_runoff"] == "socont_runoff": surface_runoff_kind = "runoff:socont" elif self.options["surface_runoff"] == "linear_storage": logger.info("Using a linear storage for the quick flow.") surface_runoff_kind = "outflow:linear" else: raise ConfigurationError( f"The surface runoff option {self.options['surface_runoff']} " f"is not recognised in Socont.", item_name="surface_runoff", item_value=self.options["surface_runoff"], reason="Invalid option", ) self.structure["surface_runoff"] = { "attach_to": "hydro_unit", "kind": "storage", "processes": {"runoff": {"kind": surface_runoff_kind, "target": "outlet"}}, } def _define_parameter_aliases(self) -> None: """ Define parameter name aliases for the Socont model. Maps component-specific parameter names to user-friendly shorthand names: - A: Soil storage capacity - k_slow/k_slow_1/k_slow1: First soil layer response factor - k_slow_2/k_slow2: Second soil layer response factor - k_snow: Snow melt storage response factor - k_ice: Glacier ice melt storage response factor - k_quick: Surface runoff response factor These aliases allow users to specify parameters using simpler names. """ self.parameter_aliases = { "slow_reservoir:capacity": "A", "slow_reservoir:response_factor": ["k_slow", "k_slow_1", "k_slow1"], "slow_reservoir_2:response_factor": ["k_slow_2", "k_slow2"], "glacier_area_rain_snowmelt_storage:response_factor": "k_snow", "glacier_area_icemelt_storage:response_factor": "k_ice", "surface_runoff:response_factor": "k_quick", } def _define_parameter_constraints(self) -> None: """ Define parameter constraints for the Socont model. Enforces physical relationships between parameters: - a_snow < a_ice: Snow melt degree-day < glacier ice melt degree-day - k_slow_1 < k_quick: Slow flow slower than quick flow - k_slow_2 < k_quick: Second soil layer slower than quick flow - k_slow_2 < k_slow_1: Second soil layer slower than first layer These constraints ensure the model behaves physically. """ self.parameter_constraints = [ ["a_snow", "<", "a_ice"], ["k_slow_1", "<", "k_quick"], ["k_slow_2", "<", "k_quick"], ["k_slow_2", "<", "k_slow_1"], ] def _set_specific_options(self, kwargs: dict[str, Any]) -> None: """ Set Socont-specific configuration options. Processes Socont model options: - soil_storage_nb: Number of soil storage layers (1 or 2) Parameters ---------- kwargs Keyword arguments containing Socont-specific options. Raises ------ ValueError If soil_storage_nb is not 1 or 2. """ if "soil_storage_nb" in kwargs: self.options["soil_storage_nb"] = int(kwargs["soil_storage_nb"]) if ( self.options["soil_storage_nb"] < 1 or self.options["soil_storage_nb"] > 2 ): raise ConfigurationError( 'The option "soil_storage_nb" can only be 1 or 2.', item_name="soil_storage_nb", item_value=self.options["soil_storage_nb"], reason="Invalid option value", )