Skip to content

Hydrology (kadi.weather.hydrology)

The Hydrology class models soil water balance for an agricultural plot. It calculates reference evapotranspiration (ET0), daily runoff (SCS-CN), and the full water balance according to the FAO-56 standard.

It is used internally by the Weather facade, but can also be instantiated directly for specific analyses.


Direct Import

from kadi.weather import Hydrology

Initialization

In standard use, Hydrology is created automatically by the Weather facade upon the first invocation of water_balance() or et0_hargreaves(). It is then accessible via weather.hydrology.

import kadi as kd

weather = kd.Weather(lat=9.3, lon=2.3, name="Parakou")

# Hydrology loaded on demand
balance = weather.water_balance(crop="maize", soil_type="ferrugineux")
hydrology = weather.hydrology   # Instance available

For direct instantiation:

from kadi.weather import Hydrology, Location
import pandas as pd

location = Location(lat=9.3, lon=2.3, name="Parakou")

# rainfall : daily pd.Series of precipitation, indexed by date
# temperature : pd.DataFrame with columns 'temperature_min' and 'temperature_max'

hydrology = Hydrology(
    location,
    rainfall,
    temperature,
    soil_type="ferrugineux",
    crop="maize",
)

Public Attributes:

Attribute Type Description
location Location Plot location
rainfall pd.Series Daily precipitation series
temperature pd.DataFrame Temperature data (temperature_min, temperature_max)
crop str Crop type
soil_type str Soil type
soil dict Soil physical parameters
balance pd.DataFrame Water balance result (None prior to calculation)

Methods

water_balance()

Simulates daily soil water balance according to the FAO-56 method.

Calculation includes:

  1. Daily ET0 by Hargreaves-Samani.
  2. Runoff by the SCS-CN method with AMC (Antecedent Moisture Condition) adjustment over the previous 5 days.
  3. Crop evapotranspiration (ETc = ET0 x Kc).
  4. Sequential balance: effective rainfall - ETc, capped at TAW (Total Available Water).
balance = weather.water_balance(crop="maize", soil_type="ferrugineux")

# Last 7 days
print(balance.tail(7)[[
    "precip", "et0", "pluie_eff",
    "evapotransp", "deficit_eau", "reserve_utile", "stress_hydrique_index"
]])

This calculation is triggered via the Weather facade. To call it directly on the Hydrology instance (for example after modifying the crop):

# Modifying crop and soil
weather.hydrology.crop = "rice"
weather.hydrology.soil_type = "ferrallitique"
weather.hydrology.soil = weather.hydrology.soil_params("ferrallitique")

# Recalculate
balance = weather.hydrology.water_balance()

Returns: pd.DataFrame with DatetimeIndex.

Columns:

Column Description
precip Observed precipitation (mm)
et0 ET0 by Hargreaves-Samani (mm/day)
pluie_eff Effective rainfall after deducting runoff (mm)
evapotransp ETc = ET0 x Kc for the crop (mm/day)
deficit_eau Daily soil water deficit (mm)
reserve_utile Available soil water = TAW - deficit (mm)
stress_hydrique_index Water stress index = deficit / TAW (0 to 1)

Exceptions:

  • DataError: missing precipitation or temperature data.

et0_hargreaves(tmin, tmax, day_of_year)

Calculates reference evapotranspiration (ET0) using the Hargreaves-Samani method. Alternative to Penman-Monteith when humidity, wind, and solar radiation data are unavailable.

# Calculation for a given day
et0 = weather.et0_hargreaves(tmin=22.0, tmax=35.0, day_of_year=180)
print(f"ET0 : {et0:.2f} mm/day")

# Direct call on Hydrology instance (also accepts numpy arrays)
import numpy as np
tmin_arr = np.array([20.0, 21.0, 19.0])
tmax_arr = np.array([33.0, 35.0, 32.0])
doy_arr  = np.array([150, 151, 152])
et0_arr = weather.hydrology.et0_hargreaves(tmin_arr, tmax_arr, doy_arr)

Parameters:

Name Type Description
tmin float or np.ndarray Minimum temperature (degC)
tmax float or np.ndarray Maximum temperature (degC)
day_of_year int or np.ndarray Day of year (1 to 365)

Returns: float or np.ndarray - ET0 in mm/day.


et0_fao56_penman(tmin, tmax, humidity, wind_speed, solar_rad)

Calculates ET0 using the FAO-56 Penman-Monteith method. More accurate than Hargreaves as it integrates relative humidity, wind speed, and measured solar radiation.

et0_pm = weather.hydrology.et0_fao56_penman(
    tmin=22.0,
    tmax=35.0,
    humidity=65.0,     # Relative humidity (%)
    wind_speed=2.5,    # Wind speed at 2 m (m/s)
    solar_rad=18.0,    # Solar radiation (MJ/m2/day)
)
print(f"Penman-Monteith ET0 : {et0_pm:.2f} mm/day")

Parameters:

Name Type Description
tmin float Minimum temperature (degC)
tmax float Maximum temperature (degC)
humidity float Average relative humidity (%, between 0 and 100)
wind_speed float Wind speed measured at 2 m height (m/s)
solar_rad float Incident solar radiation (MJ/m2/day)

Returns: float - ET0 in mm/day.


runoff_cn(precipitation, prior_5d_rain)

Calculates daily runoff using the revised SCS-CN method, with adjustment based on antecedent moisture condition (AMC).

runoff = weather.hydrology.runoff_cn(
    precipitation=35.0,
    prior_5d_rain=20.0,
)
print(f"Runoff : {runoff:.2f} mm")

Parameters:

Name Type Default Description
precipitation float required Daily precipitation (mm)
prior_5d_rain float 0.0 5-day prior accumulation (mm) for AMC adjustment

AMC Adjustment:

Condition Prior 5-day Rain Adjusted CN
AMC I (dry) < 12.5 mm Reduced CN
AMC II (average) 12.5 to 35.5 mm Base CN
AMC III (wet) > 35.5 mm Increased CN

Returns: float - Runoff in mm (0.0 if rainfall does not exceed initial abstraction).


soil_params(soil_type)

Returns physical parameters for a Beninese soil.

params = weather.hydrology.soil_params("ferrallitique")
print(f"TAW   : {params['taw']} mm")
print(f"CN    : {params['cn_amc2']}")
print(f"Ksat  : {params['ksat']} mm/day")

Parameters:

Name Type Description
soil_type str Soil type among supported values

Supported Soil Types:

Type TAW (mm) CN AMC II Ksat (mm/day)
'ferrugineux' 100 82 15
'ferrallitique' 130 75 35
'sableux' 60 65 100
'limoneux' 150 78 10

Returns: dict with taw, cn_amc2, ksat.

Exceptions:

  • ValidationError: unsupported soil type.

crop_kc(crop, stage)

Returns crop coefficient (Kc) according to phenological stage. The Kc represents the ratio ETc / ET0 according to FAO-56 standard.

kc = weather.hydrology.crop_kc("maize", "mid")
print(f"Mid-season Kc : {kc}")

Parameters:

Name Type Description
crop str Crop name
stage str Stage among 'ini', 'mid', 'end'

Kc Coefficients by Crop and Stage:

Crop Kc ini Kc mid Kc end
'maize' 0.30 1.20 0.35
'rice' 1.05 1.20 0.90
'manioc' 0.30 0.80 0.30
'sorghum' 0.30 1.00 0.55
'tomato' 0.60 1.15 0.70

Returns: float - Kc value.

Exceptions:

  • CropError: unrecognized crop.

Deprecated Methods

Legacy Method Replaced By Since
compute_water_balance() water_balance() v1.2.0
get_soil_params(soil_type) soil_params(soil_type) v1.2.0
get_crop_coefficients(crop, stage) crop_kc(crop, stage) v1.2.0

Deprecated Attributes

Legacy Attribute Replaced By Since
rainfall_data rainfall v1.2.0
temperature_data temperature v1.2.0
balance_result balance v1.2.0

Complete Example

import kadi as kd

weather = kd.Weather(lat=9.3, lon=2.3, name="Parakou")

# Full water balance
balance = weather.water_balance(crop="sorghum", soil_type="sableux")

# Water stress analysis over last 30 days
last_30d = balance.tail(30)
mean_stress = last_30d["stress_hydrique_index"].mean()
print(f"Average water stress (30d) : {mean_stress:.2f}")

# Single-day ET0
et0 = weather.et0_hargreaves(tmin=21.0, tmax=34.0, day_of_year=200)
print(f"Daily ET0 : {et0:.2f} mm/day")

# Soil parameters
params = weather.hydrology.soil_params("ferrugineux")
print(f"Max available water : {params['taw']} mm")

# Mid-season Kc
kc = weather.hydrology.crop_kc("sorghum", "mid")
print(f"ETc = ET0 x Kc = {et0:.2f} x {kc} = {et0 * kc:.2f} mm/day")

Hydrology

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

Attributs

location (Location): Localisation de la parcelle. rainfall (pd.Series): Série de précipitations quotidiennes. temperature (pd.DataFrame): DataFrame avec temperature_min et temperature_max. crop (str): Type de culture. soil_type (str): Type de sol. soil (dict): Paramètres physiques du sol. balance (pd.DataFrame): Résultat du bilan hydrique (None avant calcul).

Source code in kadi/weather/hydrology.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
class Hydrology:
    """
    Gère la modélisation hydrologique (bilan hydrique du sol) pour une
    parcelle.

    Attributs:
        location (Location): Localisation de la parcelle.
        rainfall (pd.Series): Série de précipitations quotidiennes.
        temperature (pd.DataFrame): DataFrame avec temperature_min et
            temperature_max.
        crop (str): Type de culture.
        soil_type (str): Type de sol.
        soil (dict): Paramètres physiques du sol.
        balance (pd.DataFrame): Résultat du bilan hydrique (None avant calcul).
    """

    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.

        Args:
            location (Location): Instance de Location.
            rainfall_data (pd.Series): Série de précipitations quotidiennes.
            temperature_data (pd.DataFrame): DataFrame avec les colonnes
                'temperature_min' et 'temperature_max'.
            soil_type (str): Type de sol ('ferrugineux', 'ferrallitique',
                'sableux', 'limoneux'). Si None, résolu depuis le cache.
            crop (str): Type de culture.
        """
        self.location = location
        # Nouveaux noms d'attributs
        self.rainfall = rainfall_data
        self.temperature = temperature_data
        self.crop = crop
        self.soil_type = soil_type or self._resolve_soil(location)
        # Résultat du bilan hydrique (None avant le premier calcul)
        self.balance: Optional[pd.DataFrame] = None

        # Paramètres physiques du sol associé
        self.soil = self.soil_params(self.soil_type)

    def _resolve_soil(self, location: Location) -> str:
        """
        Détermine le type de sol depuis le cache local pré-téléchargé.

        Args:
            location (Location): Localisation de la parcelle.

        Returns:
            str: Identifiant du type de sol le plus probable.
        """
        from kadi._sources.soilgrids import fetch_soil_type
        return fetch_soil_type(location.lat, location.lon)

    def et0_hargreaves(self, tmin, tmax, day_of_year) -> 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.

        Args:
            tmin: Température minimale (degC).
            tmax: Température maximale (degC).
            day_of_year: Jour de l'année (1 à 365).

        Returns:
            float: ETo en mm/jour.
        """
        # 1. Calcul du rayonnement extraterrestre Ra
        lat_rad = np.radians(self.location.lat)
        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 = (
            np.maximum(0.0, tmax - tmin)
            if isinstance(tmax, np.ndarray)
            else max(0.0, tmax - tmin)
        )
        k_rs = 0.0023

        eto = 0.408 * k_rs * ra * (tmean + 17.8) * (tdiff ** 0.5)
        if isinstance(eto, np.ndarray):
            return np.maximum(0.0, eto)
        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é.

        Args:
            tmin (float): Température minimale (degC).
            tmax (float): Température maximale (degC).
            humidity (float): Humidité relative moyenne (%, entre 0 et 100).
            wind_speed (float): Vitesse du vent mesurée à 2 m de hauteur (m/s).
            solar_rad (float): Rayonnement solaire incident (MJ/m2/jour).

        Returns:
            float: ETo en mm/jour.
        """
        # Altitude moyenne estimée au Bénin (m) 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/degC) (FAO-56 Eq. 8)
        gamma = 0.000665 * p_atm

        # Pente de la courbe de pression de vapeur saturante (kPa/degC) (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 Tmin/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/m2/jour) : simplification Rns - Rnl
        rns = (1.0 - 0.23) * solar_rad  # albédo = 0.23 pour culture de référence
        rnl = 0.2 * solar_rad            # approximation simplifiée
        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.

        Args:
            precipitation (float): Précipitation du jour (mm).
            prior_5d_rain (float): Pluie des 5 jours précédents (pour
                ajustement AMC, Antecedent Moisture Condition).

        Returns:
            float: Ruissellement en mm.
        """
        if precipitation <= 0.0:
            return 0.0

        base_cn = self.soil["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 water_balance(self) -> pd.DataFrame:
        """
        Simule le bilan hydrique quotidien du sol selon FAO-56.

        Returns:
            pd.DataFrame: DataFrame contenant l'évolution journalière du bilan
                hydrique (précipitation, ET0, pluie effective, évapotranspiration
                réelle, déficit, réserve utile, stress hydrique).

        Raises:
            DataError: Si les données météorologiques sont absentes.
        """
        if self.rainfall.empty or self.temperature.empty:
            raise DataError(
                "Données météorologiques historiques manquantes pour le "
                "calcul du bilan hydrique."
            )

        taw = self.soil["taw"]  # Total Available Water
        base_cn = self.soil["cn_amc2"]
        kc = self.crop_kc(self.crop, "mid")

        precip_arr = self.rainfall.to_numpy()
        tmin_arr = self.temperature["temperature_min"].to_numpy()
        tmax_arr = self.temperature["temperature_max"].to_numpy()
        dates = self.rainfall.index
        dayofyear_arr = dates.dayofyear.to_numpy()

        # Calcul des pluies accumulées sur les 5 jours précédents
        prior_5d_arr = (
            self.rainfall.shift(1)
            .rolling(window=5, min_periods=0)
            .sum()
            .fillna(0.0)
            .to_numpy()
        )

        # Vectorisation ETo Hargreaves-Samani
        eto_arr = self.et0_hargreaves(tmin_arr, tmax_arr, dayofyear_arr)
        etc_arr = eto_arr * kc

        # Vectorisation du ruissellement SCS-CN
        cn_1 = base_cn / (2.281 - 0.0128 * base_cn)
        cn_3 = base_cn / (0.427 + 0.00573 * base_cn)
        cn_arr = np.where(
            prior_5d_arr < 12.5, cn_1,
            np.where(prior_5d_arr > 35.5, cn_3, base_cn),
        )
        s_arr = (25400.0 / cn_arr) - 254.0
        ia_arr = 0.2 * s_arr

        runoff_arr = np.where(
            (precip_arr > 0) & (precip_arr > ia_arr),
            ((precip_arr - ia_arr) ** 2) / (precip_arr + 0.8 * s_arr),
            0.0,
        )
        pluie_eff_arr = np.maximum(0.0, precip_arr - runoff_arr)

        # Calcul séquentiel du stock d'eau du sol
        n = len(dates)
        dr_arr = np.zeros(n)
        reserve_arr = np.zeros(n)
        stress_arr = np.zeros(n)
        dr = 0.0

        for i in range(n):
            temp_dr = dr - pluie_eff_arr[i]
            if temp_dr < 0:
                dr = 0.0
            else:
                dr = min(taw, temp_dr + etc_arr[i])
            dr_arr[i] = dr
            reserve_arr[i] = taw - dr
            stress_arr[i] = dr / taw if taw > 0 else 0.0

        df = pd.DataFrame(
            {
                "precip": precip_arr,
                "et0": np.round(eto_arr, 2),
                "pluie_eff": np.round(pluie_eff_arr, 2),
                "evapotransp": np.round(etc_arr, 2),
                "deficit_eau": np.round(dr_arr, 2),
                "reserve_utile": np.round(reserve_arr, 2),
                "stress_hydrique_index": np.round(stress_arr, 2),
            },
            index=dates,
        )

        self.balance = df
        return df

    def soil_params(self, soil_type: str) -> dict:
        """
        Retourne les paramètres physiques du sol béninois.

        AWC : Available Water Capacity.
        CN  : Curve Number (SCS-CN).
        Ksat: Conductivité hydraulique saturée.

        Args:
            soil_type (str): Type de sol parmi 'ferrugineux', 'ferrallitique',
                'sableux', 'limoneux'.

        Returns:
            dict: Dictionnaire des paramètres (taw, cn_amc2, ksat).

        Raises:
            ValidationError: Si le type de sol n'est pas pris en charge.
        """
        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 crop_kc(self, crop: str, stage: str) -> float:
        """
        Retourne le coefficient cultural (Kc) selon le stade phenologique.

        La valeur Kc représente le rapport ETC / ET0 selon la norme FAO-56
        (Allen et al., 1998).

        Args:
            crop (str): Nom de la culture.
            stage (str): Stade phenologique parmi 'ini', 'mid', 'end'.

        Returns:
            float: Coefficient Kc pour le stade spécifié.

        Raises:
            CropError: Si la culture n'est pas reconnue.
        """
        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 CropError(
                f"Culture non reconnue pour le coefficient FAO-56 : {crop}"
            )
        return kcs[crop].get(stage, 1.0)

    # ----------------------------------------------------------------
    # Méthodes dépréciées (anciens noms publics)
    # ----------------------------------------------------------------

    def compute_water_balance(self) -> pd.DataFrame:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``water_balance()`` à la place.

        Returns:
            pd.DataFrame: Résultat du bilan hydrique.
        """
        warnings.warn(
            "Hydrology.compute_water_balance() est obsolète et sera supprimé "
            "dans KadiPy v2.0. Utilisez water_balance() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.water_balance()

    def get_soil_params(self, soil_type: str) -> dict:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``soil_params(soil_type)`` à la place.

        Args:
            soil_type (str): Type de sol.

        Returns:
            dict: Paramètres du sol.
        """
        warnings.warn(
            "Hydrology.get_soil_params() est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez soil_params() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.soil_params(soil_type)

    def get_crop_coefficients(self, crop: str, stage: str) -> float:
        """
        Ancienne méthode publique. Dépréciée depuis v1.2.0.

        Utilisez ``crop_kc(crop, stage)`` à la place.

        Args:
            crop (str): Nom de la culture.
            stage (str): Stade phenologique.

        Returns:
            float: Coefficient Kc.
        """
        warnings.warn(
            "Hydrology.get_crop_coefficients() est obsolète et sera supprimé "
            "dans KadiPy v2.0. Utilisez crop_kc() à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.crop_kc(crop, stage)

    # ----------------------------------------------------------------
    # Propriétés de rétrocompatibilité (anciens noms d'attributs)
    # ----------------------------------------------------------------

    @property
    def rainfall_data(self) -> pd.Series:
        """Ancien nom de ``rainfall``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Hydrology.rainfall_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology.rainfall à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.rainfall

    @rainfall_data.setter
    def rainfall_data(self, value: pd.Series) -> None:
        """Ancien setter de ``rainfall``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Hydrology.rainfall_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology.rainfall à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.rainfall = value

    @property
    def temperature_data(self) -> pd.DataFrame:
        """Ancien nom de ``temperature``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Hydrology.temperature_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology.temperature à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.temperature

    @temperature_data.setter
    def temperature_data(self, value: pd.DataFrame) -> None:
        """Ancien setter de ``temperature``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Hydrology.temperature_data est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology.temperature à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.temperature = value

    @property
    def balance_result(self) -> Optional[pd.DataFrame]:
        """Ancien nom de ``balance``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Hydrology.balance_result est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology.balance à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.balance

    @balance_result.setter
    def balance_result(self, value: Optional[pd.DataFrame]) -> None:
        """Ancien setter de ``balance``. Déprécié depuis v1.2.0."""
        warnings.warn(
            "Hydrology.balance_result est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology.balance à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.balance = value

    @property
    def soil_params_attr(self) -> dict:
        """Ancien attribut ``soil_params`` (maintenant méthode). Interne."""
        return self.soil

    def _resolve_soil_type_from_cache(self, location: Location) -> str:
        """
        Ancien nom de ``_resolve_soil``. Déprécié depuis v1.2.0.

        Args:
            location (Location): Instance de Location.

        Returns:
            str: Type de sol.
        """
        warnings.warn(
            "Hydrology._resolve_soil_type_from_cache est obsolète et sera supprimé dans "
            "KadiPy v2.0. Utilisez Hydrology._resolve_soil à la place.",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self._resolve_soil(location)

rainfall_data property writable

rainfall_data: Series

Ancien nom de rainfall. Déprécié depuis v1.2.0.

temperature_data property writable

temperature_data: DataFrame

Ancien nom de temperature. Déprécié depuis v1.2.0.

balance_result property writable

balance_result: Optional[DataFrame]

Ancien nom de balance. Déprécié depuis v1.2.0.

soil_params_attr property

soil_params_attr: dict

Ancien attribut soil_params (maintenant méthode). Interne.

__init__

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

Initialise l'analyseur hydrologique.

Parameters:

Name Type Description Default
location Location

Instance de Location.

required
rainfall_data Series

Série de précipitations quotidiennes.

required
temperature_data DataFrame

DataFrame avec les colonnes 'temperature_min' et 'temperature_max'.

required
soil_type str

Type de sol ('ferrugineux', 'ferrallitique', 'sableux', 'limoneux'). Si None, résolu depuis le cache.

None
crop str

Type de culture.

'maize'
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.

    Args:
        location (Location): Instance de Location.
        rainfall_data (pd.Series): Série de précipitations quotidiennes.
        temperature_data (pd.DataFrame): DataFrame avec les colonnes
            'temperature_min' et 'temperature_max'.
        soil_type (str): Type de sol ('ferrugineux', 'ferrallitique',
            'sableux', 'limoneux'). Si None, résolu depuis le cache.
        crop (str): Type de culture.
    """
    self.location = location
    # Nouveaux noms d'attributs
    self.rainfall = rainfall_data
    self.temperature = temperature_data
    self.crop = crop
    self.soil_type = soil_type or self._resolve_soil(location)
    # Résultat du bilan hydrique (None avant le premier calcul)
    self.balance: Optional[pd.DataFrame] = None

    # Paramètres physiques du sol associé
    self.soil = self.soil_params(self.soil_type)

et0_hargreaves

et0_hargreaves(tmin, tmax, day_of_year) -> 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.

Parameters:

Name Type Description Default
tmin

Température minimale (degC).

required
tmax

Température maximale (degC).

required
day_of_year

Jour de l'année (1 à 365).

required

Returns:

Name Type Description
float float

ETo en mm/jour.

Source code in kadi/weather/hydrology.py
def et0_hargreaves(self, tmin, tmax, day_of_year) -> 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.

    Args:
        tmin: Température minimale (degC).
        tmax: Température maximale (degC).
        day_of_year: Jour de l'année (1 à 365).

    Returns:
        float: ETo en mm/jour.
    """
    # 1. Calcul du rayonnement extraterrestre Ra
    lat_rad = np.radians(self.location.lat)
    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 = (
        np.maximum(0.0, tmax - tmin)
        if isinstance(tmax, np.ndarray)
        else max(0.0, tmax - tmin)
    )
    k_rs = 0.0023

    eto = 0.408 * k_rs * ra * (tmean + 17.8) * (tdiff ** 0.5)
    if isinstance(eto, np.ndarray):
        return np.maximum(0.0, eto)
    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é.

Parameters:

Name Type Description Default
tmin float

Température minimale (degC).

required
tmax float

Température maximale (degC).

required
humidity float

Humidité relative moyenne (%, entre 0 et 100).

required
wind_speed float

Vitesse du vent mesurée à 2 m de hauteur (m/s).

required
solar_rad float

Rayonnement solaire incident (MJ/m2/jour).

required

Returns:

Name Type Description
float float

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é.

    Args:
        tmin (float): Température minimale (degC).
        tmax (float): Température maximale (degC).
        humidity (float): Humidité relative moyenne (%, entre 0 et 100).
        wind_speed (float): Vitesse du vent mesurée à 2 m de hauteur (m/s).
        solar_rad (float): Rayonnement solaire incident (MJ/m2/jour).

    Returns:
        float: ETo en mm/jour.
    """
    # Altitude moyenne estimée au Bénin (m) 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/degC) (FAO-56 Eq. 8)
    gamma = 0.000665 * p_atm

    # Pente de la courbe de pression de vapeur saturante (kPa/degC) (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 Tmin/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/m2/jour) : simplification Rns - Rnl
    rns = (1.0 - 0.23) * solar_rad  # albédo = 0.23 pour culture de référence
    rnl = 0.2 * solar_rad            # approximation simplifiée
    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.

Parameters:

Name Type Description Default
precipitation float

Précipitation du jour (mm).

required
prior_5d_rain float

Pluie des 5 jours précédents (pour ajustement AMC, Antecedent Moisture Condition).

0.0

Returns:

Name Type Description
float float

Ruissellement en 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.

    Args:
        precipitation (float): Précipitation du jour (mm).
        prior_5d_rain (float): Pluie des 5 jours précédents (pour
            ajustement AMC, Antecedent Moisture Condition).

    Returns:
        float: Ruissellement en mm.
    """
    if precipitation <= 0.0:
        return 0.0

    base_cn = self.soil["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

water_balance

water_balance() -> pd.DataFrame

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

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame contenant l'évolution journalière du bilan hydrique (précipitation, ET0, pluie effective, évapotranspiration réelle, déficit, réserve utile, stress hydrique).

Raises:

Type Description
DataError

Si les données météorologiques sont absentes.

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

    Returns:
        pd.DataFrame: DataFrame contenant l'évolution journalière du bilan
            hydrique (précipitation, ET0, pluie effective, évapotranspiration
            réelle, déficit, réserve utile, stress hydrique).

    Raises:
        DataError: Si les données météorologiques sont absentes.
    """
    if self.rainfall.empty or self.temperature.empty:
        raise DataError(
            "Données météorologiques historiques manquantes pour le "
            "calcul du bilan hydrique."
        )

    taw = self.soil["taw"]  # Total Available Water
    base_cn = self.soil["cn_amc2"]
    kc = self.crop_kc(self.crop, "mid")

    precip_arr = self.rainfall.to_numpy()
    tmin_arr = self.temperature["temperature_min"].to_numpy()
    tmax_arr = self.temperature["temperature_max"].to_numpy()
    dates = self.rainfall.index
    dayofyear_arr = dates.dayofyear.to_numpy()

    # Calcul des pluies accumulées sur les 5 jours précédents
    prior_5d_arr = (
        self.rainfall.shift(1)
        .rolling(window=5, min_periods=0)
        .sum()
        .fillna(0.0)
        .to_numpy()
    )

    # Vectorisation ETo Hargreaves-Samani
    eto_arr = self.et0_hargreaves(tmin_arr, tmax_arr, dayofyear_arr)
    etc_arr = eto_arr * kc

    # Vectorisation du ruissellement SCS-CN
    cn_1 = base_cn / (2.281 - 0.0128 * base_cn)
    cn_3 = base_cn / (0.427 + 0.00573 * base_cn)
    cn_arr = np.where(
        prior_5d_arr < 12.5, cn_1,
        np.where(prior_5d_arr > 35.5, cn_3, base_cn),
    )
    s_arr = (25400.0 / cn_arr) - 254.0
    ia_arr = 0.2 * s_arr

    runoff_arr = np.where(
        (precip_arr > 0) & (precip_arr > ia_arr),
        ((precip_arr - ia_arr) ** 2) / (precip_arr + 0.8 * s_arr),
        0.0,
    )
    pluie_eff_arr = np.maximum(0.0, precip_arr - runoff_arr)

    # Calcul séquentiel du stock d'eau du sol
    n = len(dates)
    dr_arr = np.zeros(n)
    reserve_arr = np.zeros(n)
    stress_arr = np.zeros(n)
    dr = 0.0

    for i in range(n):
        temp_dr = dr - pluie_eff_arr[i]
        if temp_dr < 0:
            dr = 0.0
        else:
            dr = min(taw, temp_dr + etc_arr[i])
        dr_arr[i] = dr
        reserve_arr[i] = taw - dr
        stress_arr[i] = dr / taw if taw > 0 else 0.0

    df = pd.DataFrame(
        {
            "precip": precip_arr,
            "et0": np.round(eto_arr, 2),
            "pluie_eff": np.round(pluie_eff_arr, 2),
            "evapotransp": np.round(etc_arr, 2),
            "deficit_eau": np.round(dr_arr, 2),
            "reserve_utile": np.round(reserve_arr, 2),
            "stress_hydrique_index": np.round(stress_arr, 2),
        },
        index=dates,
    )

    self.balance = df
    return df

soil_params

soil_params(soil_type: str) -> dict

Retourne les paramètres physiques du sol béninois.

AWC : Available Water Capacity. CN : Curve Number (SCS-CN). Ksat: Conductivité hydraulique saturée.

Parameters:

Name Type Description Default
soil_type str

Type de sol parmi 'ferrugineux', 'ferrallitique', 'sableux', 'limoneux'.

required

Returns:

Name Type Description
dict dict

Dictionnaire des paramètres (taw, cn_amc2, ksat).

Raises:

Type Description
ValidationError

Si le type de sol n'est pas pris en charge.

Source code in kadi/weather/hydrology.py
def soil_params(self, soil_type: str) -> dict:
    """
    Retourne les paramètres physiques du sol béninois.

    AWC : Available Water Capacity.
    CN  : Curve Number (SCS-CN).
    Ksat: Conductivité hydraulique saturée.

    Args:
        soil_type (str): Type de sol parmi 'ferrugineux', 'ferrallitique',
            'sableux', 'limoneux'.

    Returns:
        dict: Dictionnaire des paramètres (taw, cn_amc2, ksat).

    Raises:
        ValidationError: Si le type de sol n'est pas pris en charge.
    """
    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]

crop_kc

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

Retourne le coefficient cultural (Kc) selon le stade phenologique.

La valeur Kc représente le rapport ETC / ET0 selon la norme FAO-56 (Allen et al., 1998).

Parameters:

Name Type Description Default
crop str

Nom de la culture.

required
stage str

Stade phenologique parmi 'ini', 'mid', 'end'.

required

Returns:

Name Type Description
float float

Coefficient Kc pour le stade spécifié.

Raises:

Type Description
CropError

Si la culture n'est pas reconnue.

Source code in kadi/weather/hydrology.py
def crop_kc(self, crop: str, stage: str) -> float:
    """
    Retourne le coefficient cultural (Kc) selon le stade phenologique.

    La valeur Kc représente le rapport ETC / ET0 selon la norme FAO-56
    (Allen et al., 1998).

    Args:
        crop (str): Nom de la culture.
        stage (str): Stade phenologique parmi 'ini', 'mid', 'end'.

    Returns:
        float: Coefficient Kc pour le stade spécifié.

    Raises:
        CropError: Si la culture n'est pas reconnue.
    """
    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 CropError(
            f"Culture non reconnue pour le coefficient FAO-56 : {crop}"
        )
    return kcs[crop].get(stage, 1.0)

compute_water_balance

compute_water_balance() -> pd.DataFrame

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez water_balance() à la place.

Returns:

Type Description
DataFrame

pd.DataFrame: Résultat du bilan hydrique.

Source code in kadi/weather/hydrology.py
def compute_water_balance(self) -> pd.DataFrame:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``water_balance()`` à la place.

    Returns:
        pd.DataFrame: Résultat du bilan hydrique.
    """
    warnings.warn(
        "Hydrology.compute_water_balance() est obsolète et sera supprimé "
        "dans KadiPy v2.0. Utilisez water_balance() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.water_balance()

get_soil_params

get_soil_params(soil_type: str) -> dict

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez soil_params(soil_type) à la place.

Parameters:

Name Type Description Default
soil_type str

Type de sol.

required

Returns:

Name Type Description
dict dict

Paramètres du sol.

Source code in kadi/weather/hydrology.py
def get_soil_params(self, soil_type: str) -> dict:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``soil_params(soil_type)`` à la place.

    Args:
        soil_type (str): Type de sol.

    Returns:
        dict: Paramètres du sol.
    """
    warnings.warn(
        "Hydrology.get_soil_params() est obsolète et sera supprimé dans "
        "KadiPy v2.0. Utilisez soil_params() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.soil_params(soil_type)

get_crop_coefficients

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

Ancienne méthode publique. Dépréciée depuis v1.2.0.

Utilisez crop_kc(crop, stage) à la place.

Parameters:

Name Type Description Default
crop str

Nom de la culture.

required
stage str

Stade phenologique.

required

Returns:

Name Type Description
float float

Coefficient Kc.

Source code in kadi/weather/hydrology.py
def get_crop_coefficients(self, crop: str, stage: str) -> float:
    """
    Ancienne méthode publique. Dépréciée depuis v1.2.0.

    Utilisez ``crop_kc(crop, stage)`` à la place.

    Args:
        crop (str): Nom de la culture.
        stage (str): Stade phenologique.

    Returns:
        float: Coefficient Kc.
    """
    warnings.warn(
        "Hydrology.get_crop_coefficients() est obsolète et sera supprimé "
        "dans KadiPy v2.0. Utilisez crop_kc() à la place.",
        category=DeprecationWarning,
        stacklevel=2,
    )
    return self.crop_kc(crop, stage)