Aller au contenu

Hydrologie (kadi.weather.hydrology)

Le module Hydrology modélise le cycle de l'eau dans le sol selon les normes de la FAO (publication FAO-56). Il calcule l'évapotranspiration de référence (ET0) par la méthode Hargreaves-Samani et simule le bilan hydrique journalier.


Évapotranspiration de référence (ET0)

L'ET0 représente la quantité d'eau évaporée et transpirée par une surface de gazon bien alimentée en eau. C'est la référence à partir de laquelle on calcule les besoins en eau de chaque culture (ETc = ET0 × Kc).

Méthode Hargreaves-Samani

Cette méthode ne demande que les températures min et max, ce qui la rend utilisable même quand le rayonnement solaire n'est pas disponible.

Formule :

ET0 = 0.0023 × Ra × (T_moy + 17.8) × (T_max - T_min)^0.5

Ra est le rayonnement extraterrestre (MJ/m²/jour), calculé à partir de la latitude et du jour de l'année.

et0 = session.et0_hargreaves(tmin=22.0, tmax=35.0, day_of_year=200)
print(f"ET0 : {et0:.2f} mm/jour")

Bilan hydrique (FAO-56)

Le bilan hydrique suit l'eau disponible dans le sol au fil du temps, en tenant compte des apports (pluie) et des pertes (évapotranspiration, ruissellement).

Équation journalière :

ΔRU = Pluie - ETc - Ruissellement
RU_j+1 = max(0, min(RU_j + ΔRU, RU_max))

Où : - RU = Réserve Utile en eau du sol (mm) - ETc = Évapotranspiration de la culture = ET0 × Kc - RU_max = Capacité maximale de rétention du sol

Types de sols béninois supportés

Code Nom RU max (mm) Ksat (mm/h)
ferrugineux Sol ferrugineux tropical 80 15
vertisol Vertisol argileux 120 5
hydromorphe Sol hydromorphe 150 2
sableux Sol sableux 50 40

Utilisation

Bilan hydrique complet

from kadi.weather import WeatherSession

session = WeatherSession(latitude=9.3333, longitude=2.6333, name="Parakou")

# Bilan hydrique pour le maïs sur sol ferrugineux
bilan = session.water_balance(crop="maize", soil_type="ferrugineux")

# Affichage des 14 derniers jours
print(bilan.tail(14)[[
    "precipitation", "ET0", "ETc", "deficit_eau", "reserve_utile", "runoff"
]])

Interprétation des résultats

import pandas as pd

# Jours en situation de stress hydrique (déficit > 5 mm)
stress = bilan[bilan["deficit_eau"] > 5.0]
print(f"Jours de stress hydrique : {len(stress)}")

# Calcul du déficit cumulé sur la saison
deficit_cumule = bilan["deficit_eau"].sum()
print(f"Déficit hydrique cumulé : {deficit_cumule:.1f} mm")

# Période critique (réserve utile < 20% de la capacité)
ru_max = 80  # mm pour sol ferrugineux
periode_critique = bilan[bilan["reserve_utile"] < ru_max * 0.2]
print(f"Jours critiques : {len(periode_critique)}")

ET0 journalier direct

# Calcul de l'ET0 pour un jour spécifique de l'année
# Jour 200 = 19 juillet (pic de saison des pluies)
et0_juillet = session.et0_hargreaves(
    tmin=22.0,
    tmax=35.0,
    day_of_year=200,
)
print(f"ET0 mi-juillet : {et0_juillet:.2f} mm/jour")

# Jour 365 = 31 décembre (saison sèche)
et0_decembre = session.et0_hargreaves(
    tmin=18.0,
    tmax=38.0,
    day_of_year=365,
)
print(f"ET0 fin décembre : {et0_decembre:.2f} mm/jour")

Colonnes du DataFrame de bilan hydrique

Colonne Unité Description
precipitation mm Précipitations journalières observées
temperature_min °C Température minimale
temperature_max °C Température maximale
ET0 mm Évapotranspiration de référence (Hargreaves)
Kc Coefficient cultural de la culture sélectionnée
ETc mm Évapotranspiration de la culture = ET0 × Kc
deficit_eau mm Manque d'eau journalier (max(0, ETc - Pluie))
reserve_utile mm Eau disponible dans la réserve utile du sol
runoff mm Eau non infiltrée (ruissellement)

Perspectives

La version actuelle utilise Hargreaves-Samani, méthode robuste ne nécessitant que les températures. La méthode Penman-Monteith (FAO-56 complète) sera intégrée dans une version future en exploitant le rayonnement solaire et la vitesse du vent disponibles via Open-Meteo.


Hydrology

Gère la modélisation hydrologique (bilan hydrique du sol) pour une parcelle.

Source code in kadi/weather/hydrology.py
class Hydrology:
    """
    Gère la modélisation hydrologique (bilan hydrique du sol) pour une parcelle.
    """

    def __init__(self, location: Location, rainfall_data: pd.Series, temperature_data: pd.DataFrame, soil_type: Optional[str] = None, crop: str = 'maize'):
        """
        Initialise l'analyseur hydrologique.

        :param location: Instance de Location.
        :param rainfall_data: Série pandas de précipitations quotidiennes.
        :param temperature_data: DataFrame avec les colonnes 'temperature_min' et 'temperature_max'.
        :param soil_type: Type de sol ('ferrugineux', 'ferrallitique', 'sableux', 'limoneux').
        :param crop: Type de culture.
        """
        self.location = location
        self.rainfall_data = rainfall_data
        self.temperature_data = temperature_data
        self.crop = crop
        self.soil_type = soil_type or self._resolve_soil_type_from_cache(location)
        self.balance_result: Optional[pd.DataFrame] = None

        self.soil_params = self.get_soil_params(self.soil_type)

    def _resolve_soil_type_from_cache(self, location: Location) -> str:
        """Détermine le type de sol depuis le cache local pré-téléchargé."""
        from kadi._sources.soilgrids import fetch_soil_type
        return fetch_soil_type(location.latitude, location.longitude)

    def et0_hargreaves(self, tmin: float, tmax: float, day_of_year: int) -> float:
        """
        Calcule l'évapotranspiration de référence (ETo) par Hargreaves-Samani.

        Méthode alternative à Penman-Monteith, utilisée en l'absence de données
        d'humidité, de vent et de rayonnement solaire.

        :param tmin: Température minimale (°C).
        :param tmax: Température maximale (°C).
        :param day_of_year: Jour de l'année (1-365).
        :return: ETo en mm/jour.
        """
        # 1. Calcul du rayonnement extraterrestre Ra
        lat_rad = np.radians(self.location.latitude)
        dr = 1 + 0.033 * np.cos(2 * np.pi * day_of_year / 365.0)
        delta = 0.409 * np.sin(2 * np.pi * day_of_year / 365.0 - 1.39)

        cos_omega_s = -np.tan(lat_rad) * np.tan(delta)
        cos_omega_s = np.clip(cos_omega_s, -1.0, 1.0)
        omega_s = np.arccos(cos_omega_s)

        # Constante solaire (MJ/m2/min)
        gsc = 0.0820
        ra = (24 * 60 / np.pi) * gsc * dr * (
            omega_s * np.sin(lat_rad) * np.sin(delta)
            + np.cos(lat_rad) * np.cos(delta) * np.sin(omega_s)
        )

        # 2. Formule Hargreaves-Samani
        tmean = (tmax + tmin) / 2.0
        tdiff = max(0.0, tmax - tmin)
        k_rs = 0.0023

        eto = 0.408 * k_rs * ra * (tmean + 17.8) * (tdiff ** 0.5)
        return float(max(0.0, eto))

    def et0_fao56_penman(
        self,
        tmin: float,
        tmax: float,
        humidity: float,
        wind_speed: float,
        solar_rad: float,
    ) -> float:
        """
        Calcule l'évapotranspiration de référence (ETo) par FAO-56 Penman-Monteith.

        Méthode de référence internationale (Allen et al., 1998, FAO-56).
        Plus précise que Hargreaves car elle intègre l'humidité relative,
        la vitesse du vent et le rayonnement solaire mesuré.

        :param tmin: Température minimale (°C).
        :param tmax: Température maximale (°C).
        :param humidity: Humidité relative moyenne (%, entre 0 et 100).
        :param wind_speed: Vitesse du vent mesurée à 2 m de hauteur (m/s).
        :param solar_rad: Rayonnement solaire incident (MJ/m²/jour).
        :return: ETo en mm/jour.
        """
        # Altitude moyenne estimée au Bénin (m) — utilisée pour la pression atmosphérique
        z = 200.0
        tmean = (tmax + tmin) / 2.0

        # Pression atmosphérique (kPa) selon l'équation standard (FAO-56 Eq. 7)
        p_atm = 101.3 * ((293.0 - 0.0065 * z) / 293.0) ** 5.26

        # Constante psychrométrique gamma (kPa/°C) (FAO-56 Eq. 8)
        gamma = 0.000665 * p_atm

        # Pente de la courbe de pression de vapeur saturante (kPa/°C) (FAO-56 Eq. 13)
        delta = 4098.0 * (0.6108 * np.exp(17.27 * tmean / (tmean + 237.3))) / (tmean + 237.3) ** 2

        # Pression de vapeur saturante (kPa) : moyenne sur Tmin et Tmax (FAO-56 Eq. 11-12)
        es_tmax = 0.6108 * np.exp(17.27 * tmax / (tmax + 237.3))
        es_tmin = 0.6108 * np.exp(17.27 * tmin / (tmin + 237.3))
        es = (es_tmax + es_tmin) / 2.0

        # Pression de vapeur réelle ea (kPa) depuis l'humidité relative (FAO-56 Eq. 17)
        ea = (humidity / 100.0) * es

        # Déficit de pression de vapeur (kPa)
        vpd = es - ea

        # Rayonnement net (MJ/m²/jour) : simplification Rns - Rnl
        # Rayonnement net court (albédo = 0.23 pour une culture de référence)
        rns = (1.0 - 0.23) * solar_rad
        # Rayonnement net long-onde (approximation simplifiée)
        rnl = 0.2 * solar_rad
        rn = rns - rnl

        # Flux de chaleur du sol G ≈ 0 à l'échelle journalière (FAO-56 hypothèse)
        g = 0.0

        # Équation FAO-56 Penman-Monteith (FAO-56 Eq. 6)
        num = 0.408 * delta * (rn - g) + gamma * (900.0 / (tmean + 273.0)) * wind_speed * vpd
        den = delta + gamma * (1.0 + 0.34 * wind_speed)

        eto = num / den
        return float(max(0.0, eto))


    def runoff_cn(self, precipitation: float, prior_5d_rain: float = 0.0) -> float:
        """
        Calcule le ruissellement quotidien par la méthode révisée SCS-CN.

        :param precipitation: Précipitation du jour (mm).
        :param prior_5d_rain: Pluie des 5 jours précédents (pour ajustement AMC).
        :return: Ruissellement (mm).
        """
        if precipitation <= 0.0:
            return 0.0

        base_cn = self.soil_params['cn_amc2']

        # Ajustement AMC (Antecedent Moisture Condition)
        if prior_5d_rain < 12.5:
            cn = base_cn / (2.281 - 0.0128 * base_cn) # AMC I (sec)
        elif prior_5d_rain > 35.5:
            cn = base_cn / (0.427 + 0.00573 * base_cn) # AMC III (humide)
        else:
            cn = base_cn # AMC II (moyen)

        s = (25400.0 / cn) - 254.0
        ia = 0.2 * s # Abstraction initiale

        if precipitation > ia:
            runoff = ((precipitation - ia) ** 2) / (precipitation + 0.8 * s)
            return float(runoff)
        return 0.0

    def compute_water_balance(self) -> pd.DataFrame:
        """
        Simule le bilan hydrique quotidien du sol selon FAO-56.

        :return: DataFrame contenant l'évolution du bilan.
        """
        if self.rainfall_data.empty or self.temperature_data.empty:
            raise InsufficientData("Données météorologiques historiques manquantes pour le calcul du bilan hydrique.")

        taw = self.soil_params['taw'] # Total Available Water
        dr = 0.0 # Depletion (épuisement initial, sol plein = 0)

        dates = self.rainfall_data.index
        results = []

        for i, date in enumerate(dates):
            precip = self.rainfall_data.iloc[i]
            tmin = self.temperature_data['temperature_min'].iloc[i]
            tmax = self.temperature_data['temperature_max'].iloc[i]

            # Pluie sur 5j précédents pour le CN
            if i >= 5:
                prior_5d = self.rainfall_data.iloc[i-5:i].sum()
            else:
                prior_5d = self.rainfall_data.iloc[:i].sum()

            # Ruissellement
            runoff = self.runoff_cn(precip, prior_5d)
            pluie_eff = max(0.0, precip - runoff)

            # Evapotranspiration
            et0 = self.et0_hargreaves(tmin, tmax, date.dayofyear)
            kc = self.get_crop_coefficients(self.crop, 'mid') # Simplifié pour le MVP
            etc = et0 * kc

            # Bilan
            temp_dr = dr - pluie_eff

            if temp_dr < 0:
                dr = 0.0 # Drainage profond
            else:
                dr = min(taw, temp_dr + etc)

            reserve = taw - dr
            stress_index = dr / taw if taw > 0 else 0

            results.append({
                'date': date,
                'precip': precip,
                'et0': round(et0, 2),
                'pluie_eff': round(pluie_eff, 2),
                'evapotransp': round(etc, 2),
                'deficit_eau': round(dr, 2),
                'reserve_utile': round(reserve, 2),
                'stress_hydrique_index': round(stress_index, 2)
            })

        df = pd.DataFrame(results).set_index('date')
        self.balance_result = df
        return df

    def get_soil_params(self, soil_type: str) -> dict:
        """
        Retourne les paramètres physiques du sol béninois.
        AWC: Available Water Capacity, CN: Curve Number, Ksat: conductivité.
        """
        soils = {
            'ferrugineux': {'taw': 100.0, 'cn_amc2': 82.0, 'ksat': 15.0},
            'ferrallitique': {'taw': 130.0, 'cn_amc2': 75.0, 'ksat': 35.0},
            'sableux': {'taw': 60.0, 'cn_amc2': 65.0, 'ksat': 100.0},
            'limoneux': {'taw': 150.0, 'cn_amc2': 78.0, 'ksat': 10.0}
        }
        if soil_type not in soils:
            raise ValidationError(f"Type de sol non pris en charge: {soil_type}")
        return soils[soil_type]

    def get_crop_coefficients(self, crop: str, stage: str) -> float:
        """
        Retourne le coefficient de culture (Kc) selon le stade.
        (Simplifié pour l'implémentation de base).
        """
        kcs = {
            'maize': {'ini': 0.3, 'mid': 1.2, 'end': 0.35},
            'rice': {'ini': 1.05, 'mid': 1.2, 'end': 0.9},
            'manioc': {'ini': 0.3, 'mid': 0.8, 'end': 0.3},
            'sorghum': {'ini': 0.3, 'mid': 1.0, 'end': 0.55},
            'tomato': {'ini': 0.6, 'mid': 1.15, 'end': 0.7}
        }
        if crop not in kcs:
            raise CropNotFound(f"Culture non reconnue pour le coefficient FAO-56: {crop}")
        params = kcs[crop]
        return params.get(stage, 1.0)

__init__

__init__(location: Location, rainfall_data: Series, temperature_data: DataFrame, soil_type: Optional[str] = None, crop: str = 'maize')

Initialise l'analyseur hydrologique.

:param location: Instance de Location. :param rainfall_data: Série pandas de précipitations quotidiennes. :param temperature_data: DataFrame avec les colonnes 'temperature_min' et 'temperature_max'. :param soil_type: Type de sol ('ferrugineux', 'ferrallitique', 'sableux', 'limoneux'). :param crop: Type de culture.

Source code in kadi/weather/hydrology.py
def __init__(self, location: Location, rainfall_data: pd.Series, temperature_data: pd.DataFrame, soil_type: Optional[str] = None, crop: str = 'maize'):
    """
    Initialise l'analyseur hydrologique.

    :param location: Instance de Location.
    :param rainfall_data: Série pandas de précipitations quotidiennes.
    :param temperature_data: DataFrame avec les colonnes 'temperature_min' et 'temperature_max'.
    :param soil_type: Type de sol ('ferrugineux', 'ferrallitique', 'sableux', 'limoneux').
    :param crop: Type de culture.
    """
    self.location = location
    self.rainfall_data = rainfall_data
    self.temperature_data = temperature_data
    self.crop = crop
    self.soil_type = soil_type or self._resolve_soil_type_from_cache(location)
    self.balance_result: Optional[pd.DataFrame] = None

    self.soil_params = self.get_soil_params(self.soil_type)

et0_hargreaves

et0_hargreaves(tmin: float, tmax: float, day_of_year: int) -> float

Calcule l'évapotranspiration de référence (ETo) par Hargreaves-Samani.

Méthode alternative à Penman-Monteith, utilisée en l'absence de données d'humidité, de vent et de rayonnement solaire.

:param tmin: Température minimale (°C). :param tmax: Température maximale (°C). :param day_of_year: Jour de l'année (1-365). :return: ETo en mm/jour.

Source code in kadi/weather/hydrology.py
def et0_hargreaves(self, tmin: float, tmax: float, day_of_year: int) -> float:
    """
    Calcule l'évapotranspiration de référence (ETo) par Hargreaves-Samani.

    Méthode alternative à Penman-Monteith, utilisée en l'absence de données
    d'humidité, de vent et de rayonnement solaire.

    :param tmin: Température minimale (°C).
    :param tmax: Température maximale (°C).
    :param day_of_year: Jour de l'année (1-365).
    :return: ETo en mm/jour.
    """
    # 1. Calcul du rayonnement extraterrestre Ra
    lat_rad = np.radians(self.location.latitude)
    dr = 1 + 0.033 * np.cos(2 * np.pi * day_of_year / 365.0)
    delta = 0.409 * np.sin(2 * np.pi * day_of_year / 365.0 - 1.39)

    cos_omega_s = -np.tan(lat_rad) * np.tan(delta)
    cos_omega_s = np.clip(cos_omega_s, -1.0, 1.0)
    omega_s = np.arccos(cos_omega_s)

    # Constante solaire (MJ/m2/min)
    gsc = 0.0820
    ra = (24 * 60 / np.pi) * gsc * dr * (
        omega_s * np.sin(lat_rad) * np.sin(delta)
        + np.cos(lat_rad) * np.cos(delta) * np.sin(omega_s)
    )

    # 2. Formule Hargreaves-Samani
    tmean = (tmax + tmin) / 2.0
    tdiff = max(0.0, tmax - tmin)
    k_rs = 0.0023

    eto = 0.408 * k_rs * ra * (tmean + 17.8) * (tdiff ** 0.5)
    return float(max(0.0, eto))

et0_fao56_penman

et0_fao56_penman(tmin: float, tmax: float, humidity: float, wind_speed: float, solar_rad: float) -> float

Calcule l'évapotranspiration de référence (ETo) par FAO-56 Penman-Monteith.

Méthode de référence internationale (Allen et al., 1998, FAO-56). Plus précise que Hargreaves car elle intègre l'humidité relative, la vitesse du vent et le rayonnement solaire mesuré.

:param tmin: Température minimale (°C). :param tmax: Température maximale (°C). :param humidity: Humidité relative moyenne (%, entre 0 et 100). :param wind_speed: Vitesse du vent mesurée à 2 m de hauteur (m/s). :param solar_rad: Rayonnement solaire incident (MJ/m²/jour). :return: ETo en mm/jour.

Source code in kadi/weather/hydrology.py
def et0_fao56_penman(
    self,
    tmin: float,
    tmax: float,
    humidity: float,
    wind_speed: float,
    solar_rad: float,
) -> float:
    """
    Calcule l'évapotranspiration de référence (ETo) par FAO-56 Penman-Monteith.

    Méthode de référence internationale (Allen et al., 1998, FAO-56).
    Plus précise que Hargreaves car elle intègre l'humidité relative,
    la vitesse du vent et le rayonnement solaire mesuré.

    :param tmin: Température minimale (°C).
    :param tmax: Température maximale (°C).
    :param humidity: Humidité relative moyenne (%, entre 0 et 100).
    :param wind_speed: Vitesse du vent mesurée à 2 m de hauteur (m/s).
    :param solar_rad: Rayonnement solaire incident (MJ/m²/jour).
    :return: ETo en mm/jour.
    """
    # Altitude moyenne estimée au Bénin (m) — utilisée pour la pression atmosphérique
    z = 200.0
    tmean = (tmax + tmin) / 2.0

    # Pression atmosphérique (kPa) selon l'équation standard (FAO-56 Eq. 7)
    p_atm = 101.3 * ((293.0 - 0.0065 * z) / 293.0) ** 5.26

    # Constante psychrométrique gamma (kPa/°C) (FAO-56 Eq. 8)
    gamma = 0.000665 * p_atm

    # Pente de la courbe de pression de vapeur saturante (kPa/°C) (FAO-56 Eq. 13)
    delta = 4098.0 * (0.6108 * np.exp(17.27 * tmean / (tmean + 237.3))) / (tmean + 237.3) ** 2

    # Pression de vapeur saturante (kPa) : moyenne sur Tmin et Tmax (FAO-56 Eq. 11-12)
    es_tmax = 0.6108 * np.exp(17.27 * tmax / (tmax + 237.3))
    es_tmin = 0.6108 * np.exp(17.27 * tmin / (tmin + 237.3))
    es = (es_tmax + es_tmin) / 2.0

    # Pression de vapeur réelle ea (kPa) depuis l'humidité relative (FAO-56 Eq. 17)
    ea = (humidity / 100.0) * es

    # Déficit de pression de vapeur (kPa)
    vpd = es - ea

    # Rayonnement net (MJ/m²/jour) : simplification Rns - Rnl
    # Rayonnement net court (albédo = 0.23 pour une culture de référence)
    rns = (1.0 - 0.23) * solar_rad
    # Rayonnement net long-onde (approximation simplifiée)
    rnl = 0.2 * solar_rad
    rn = rns - rnl

    # Flux de chaleur du sol G ≈ 0 à l'échelle journalière (FAO-56 hypothèse)
    g = 0.0

    # Équation FAO-56 Penman-Monteith (FAO-56 Eq. 6)
    num = 0.408 * delta * (rn - g) + gamma * (900.0 / (tmean + 273.0)) * wind_speed * vpd
    den = delta + gamma * (1.0 + 0.34 * wind_speed)

    eto = num / den
    return float(max(0.0, eto))

runoff_cn

runoff_cn(precipitation: float, prior_5d_rain: float = 0.0) -> float

Calcule le ruissellement quotidien par la méthode révisée SCS-CN.

:param precipitation: Précipitation du jour (mm). :param prior_5d_rain: Pluie des 5 jours précédents (pour ajustement AMC). :return: Ruissellement (mm).

Source code in kadi/weather/hydrology.py
def runoff_cn(self, precipitation: float, prior_5d_rain: float = 0.0) -> float:
    """
    Calcule le ruissellement quotidien par la méthode révisée SCS-CN.

    :param precipitation: Précipitation du jour (mm).
    :param prior_5d_rain: Pluie des 5 jours précédents (pour ajustement AMC).
    :return: Ruissellement (mm).
    """
    if precipitation <= 0.0:
        return 0.0

    base_cn = self.soil_params['cn_amc2']

    # Ajustement AMC (Antecedent Moisture Condition)
    if prior_5d_rain < 12.5:
        cn = base_cn / (2.281 - 0.0128 * base_cn) # AMC I (sec)
    elif prior_5d_rain > 35.5:
        cn = base_cn / (0.427 + 0.00573 * base_cn) # AMC III (humide)
    else:
        cn = base_cn # AMC II (moyen)

    s = (25400.0 / cn) - 254.0
    ia = 0.2 * s # Abstraction initiale

    if precipitation > ia:
        runoff = ((precipitation - ia) ** 2) / (precipitation + 0.8 * s)
        return float(runoff)
    return 0.0

compute_water_balance

compute_water_balance() -> pd.DataFrame

Simule le bilan hydrique quotidien du sol selon FAO-56.

:return: DataFrame contenant l'évolution du bilan.

Source code in kadi/weather/hydrology.py
def compute_water_balance(self) -> pd.DataFrame:
    """
    Simule le bilan hydrique quotidien du sol selon FAO-56.

    :return: DataFrame contenant l'évolution du bilan.
    """
    if self.rainfall_data.empty or self.temperature_data.empty:
        raise InsufficientData("Données météorologiques historiques manquantes pour le calcul du bilan hydrique.")

    taw = self.soil_params['taw'] # Total Available Water
    dr = 0.0 # Depletion (épuisement initial, sol plein = 0)

    dates = self.rainfall_data.index
    results = []

    for i, date in enumerate(dates):
        precip = self.rainfall_data.iloc[i]
        tmin = self.temperature_data['temperature_min'].iloc[i]
        tmax = self.temperature_data['temperature_max'].iloc[i]

        # Pluie sur 5j précédents pour le CN
        if i >= 5:
            prior_5d = self.rainfall_data.iloc[i-5:i].sum()
        else:
            prior_5d = self.rainfall_data.iloc[:i].sum()

        # Ruissellement
        runoff = self.runoff_cn(precip, prior_5d)
        pluie_eff = max(0.0, precip - runoff)

        # Evapotranspiration
        et0 = self.et0_hargreaves(tmin, tmax, date.dayofyear)
        kc = self.get_crop_coefficients(self.crop, 'mid') # Simplifié pour le MVP
        etc = et0 * kc

        # Bilan
        temp_dr = dr - pluie_eff

        if temp_dr < 0:
            dr = 0.0 # Drainage profond
        else:
            dr = min(taw, temp_dr + etc)

        reserve = taw - dr
        stress_index = dr / taw if taw > 0 else 0

        results.append({
            'date': date,
            'precip': precip,
            'et0': round(et0, 2),
            'pluie_eff': round(pluie_eff, 2),
            'evapotransp': round(etc, 2),
            'deficit_eau': round(dr, 2),
            'reserve_utile': round(reserve, 2),
            'stress_hydrique_index': round(stress_index, 2)
        })

    df = pd.DataFrame(results).set_index('date')
    self.balance_result = df
    return df

get_soil_params

get_soil_params(soil_type: str) -> dict

Retourne les paramètres physiques du sol béninois. AWC: Available Water Capacity, CN: Curve Number, Ksat: conductivité.

Source code in kadi/weather/hydrology.py
def get_soil_params(self, soil_type: str) -> dict:
    """
    Retourne les paramètres physiques du sol béninois.
    AWC: Available Water Capacity, CN: Curve Number, Ksat: conductivité.
    """
    soils = {
        'ferrugineux': {'taw': 100.0, 'cn_amc2': 82.0, 'ksat': 15.0},
        'ferrallitique': {'taw': 130.0, 'cn_amc2': 75.0, 'ksat': 35.0},
        'sableux': {'taw': 60.0, 'cn_amc2': 65.0, 'ksat': 100.0},
        'limoneux': {'taw': 150.0, 'cn_amc2': 78.0, 'ksat': 10.0}
    }
    if soil_type not in soils:
        raise ValidationError(f"Type de sol non pris en charge: {soil_type}")
    return soils[soil_type]

get_crop_coefficients

get_crop_coefficients(crop: str, stage: str) -> float

Retourne le coefficient de culture (Kc) selon le stade. (Simplifié pour l'implémentation de base).

Source code in kadi/weather/hydrology.py
def get_crop_coefficients(self, crop: str, stage: str) -> float:
    """
    Retourne le coefficient de culture (Kc) selon le stade.
    (Simplifié pour l'implémentation de base).
    """
    kcs = {
        'maize': {'ini': 0.3, 'mid': 1.2, 'end': 0.35},
        'rice': {'ini': 1.05, 'mid': 1.2, 'end': 0.9},
        'manioc': {'ini': 0.3, 'mid': 0.8, 'end': 0.3},
        'sorghum': {'ini': 0.3, 'mid': 1.0, 'end': 0.55},
        'tomato': {'ini': 0.6, 'mid': 1.15, 'end': 0.7}
    }
    if crop not in kcs:
        raise CropNotFound(f"Culture non reconnue pour le coefficient FAO-56: {crop}")
    params = kcs[crop]
    return params.get(stage, 1.0)