Source code for hydrobricks.models.gr6j

from __future__ import annotations

import logging
import math
from typing import Any

from hydrobricks._exceptions import ModelError
from hydrobricks.models.gr4j import GR4J
from hydrobricks.models.model import Model

logger = logging.getLogger(__name__)


[docs] class GR6J(GR4J): """GR6J daily rainfall-runoff model (Pushpalatha et al., 2011). GR6J extends GR4J with two changes aimed at improving low-flow simulation, while keeping the production store, interception, throughfall and ET identical to GR4J: - a threshold-based groundwater exchange ``F = X2 (R/X3 - X5)`` (the GR5J formulation), and - an additional exponential routing store (coefficient X6) placed in parallel to the power routing store. The UH1 output is split 60% to the power store and 40% to the exponential store. Parameters ---------- X1 : float Production store capacity [mm]. X2 : float Groundwater exchange coefficient [mm/d]. Negative = loss, positive = gain. X3 : float Routing store maximum capacity [mm]. X4 : float Unit hydrograph time base [d]. Must be > 0.5. X5 : float Groundwater exchange threshold [-]. Sets the routing-store filling ratio at which the exchange changes sign. X6 : float Exponential store coefficient [mm]. Must be > 0. Options ------- discrete : bool Build method for the production store and routing. True (default) computes them directly, reproducing the exact discrete equations. False integrates them with the ODE solver (provided for comparison). snow_melt_process : str or None Snowmelt method: None (no snow), 'melt:degree_day', 'melt:degree_day_aspect', 'melt:temperature_index', or 'melt:cemaneige'. 'melt:temperature_index' requires a 'solar_radiation' forcing, and 'melt:degree_day_aspect' an 'aspect_class' hydro unit property. snow_redistribution : str or None Optional snow redistribution process (e.g. 'transport:snow_slide'). """ def __init__(self, name: str = "gr6j", **kwargs: Any) -> None: # Mirror GR4J.__init__ so the error message and option defaults are correct # for GR6J; the structure/alias/transform hooks below are overridden. Model.__init__(self, name=name, **kwargs) # Default options self.options["discrete"] = True self.options["snow_melt_process"] = None self.options["snow_rain_process"] = None self.options["snow_redistribution"] = None self.allowed_land_cover_types = ["open"] self._set_options(kwargs) try: self._define_structure() self._generate_structure() self._define_parameter_aliases() self._define_parameter_constraints() self._define_parameter_transforms() except RuntimeError as err: raise ModelError(f"GR6J model initialization raised an exception: {err}") def _define_structure_discrete(self) -> None: """Discrete (exact) GR6J: same as GR4J but routed by GR6J routing.""" super()._define_structure_discrete() self.structure["uh_input"]["processes"]["routing"]["kind"] = "routing:gr6j" def _define_structure_solver(self) -> None: """Solver-based GR6J (for comparison): same as GR4J but with GR6J routing.""" super()._define_structure_solver() self.structure["uh_input"]["processes"]["routing"]["kind"] = "routing:gr6j" def _define_parameter_aliases(self) -> None: """Define user-friendly parameter aliases for the GR6J model (X1-X6).""" super()._define_parameter_aliases() def _define_parameter_transforms(self) -> None: """Define real <-> transformed parameter mappings for the GR6J model. Inherits the GR4J transforms (X1-X4) and adds: - X5 (exchange threshold): inverse hyperbolic sine (spans negative to positive, like X2). - X6 (exponential store coefficient): log space (X6 > 0). """ super()._define_parameter_transforms() self.parameter_transforms.update( { "uh_input:exchange_threshold": (math.asinh, math.sinh), "uh_input:exp_store_coeff": (math.log, math.exp), } )