Skip to content

Crop coefficients (Kc / CLC)

This module supports the FAO single crop coefficient method to adjust the evapotranspiration (ET) for different land-cover types. Monthly \(K_c\) values are mapped to Corine Land Cover (CLC) level 3 codes, using default mapping values (see data/kc_clc_mapping.csv file). The user can provide a custom mapping file if desired. The \(K_c\) factor is applied to the turbulent exchange coefficient \(C_H\) in the energy balance calculation, ensuring that the resulting surface temperature and ET are consistent with the land-cover type.

Background

The FAO-56 single crop coefficient methodology defines the crop evapotranspiration as:

\[ET_c = K_c \cdot ET_0\]

where:

  • \(ET_0\) is the reference evapotranspiration (calculated for a reference grass surface)
  • \(K_c\) is the crop coefficient that adjusts \(ET_0\) to account for the specific vegetation type and growth stage.

\(K_c\) depends on:

  • Vegetation structure: leaf area index, canopy height, surface roughness
  • Root depth: affects moisture availability
  • Stomatal conductance: differs between crops, forests, and urban surfaces
  • Season: growth stage / phenological calendar (expressed here as monthly variation)

How Kc is applied

When the energy balance is active (simulation.energy_balance = 1L)

The \(K_c\) factor is applied to the turbulent exchange coefficient \(C_H\) before the energy balance solver is called:

\[C_H^{\text{adj}} = K_c \cdot C_H\]

Why not multiply PET by Kc?

Scaling PET by \(K_c\) after the energy balance would keep \(T_s\) solved with the original (wrong) \(C_H\).
Because \(H\) and \(LE\) share the same \(C_H\) in the energy budget, the correct \(T_s\) depends on \(K_c \cdot C_H\), not on the bare-soil value.

When a precomputed PET raster is used, or in constant-PET mode

When the PET is obtained from the meteorological forcing data (raster NetCDF) or the energy balance is not active, \(K_c\) is applied directly to PET:

\[PET_c = K_c \cdot \text{PET}_{\text{raster/constant}}\]

This is the standard FAO-56 formula applied as a simple multiplicative factor, which is the only option when PET is treated as an external input.

When a MeteoRaster contains pet_c and/or pet variables

Raster contains Behaviour
pet_c Used directly as crop potential evapotranspiration; \(K_c\) is not applied again (it is assumed to be already embedded). The energy balance is forcibly disabled regardless of simulation.energy_balance setting.
pet The energy balance is forcibly disabled regardless of simulation.energy_balance setting, and \(K_c\) is applied (\(PET_c = K_c \cdot \text{PET}\)) before the soil water balance; \(PET_c\) is then used as input of the soil water balance module to calculate actual ET.
both pet_c and pet pet_c takes precedence and pet is ignored.
neither Normal energy-balance or constant-PET path applies (see above).

Output variable naming (pet_c)

When output_forcing_data.meteo_data is enabled, the Kc-adjusted potential evapotranspiration field is saved in meteo_forcing.nc as pet_c (crop potential evapotranspiration, \(PET_c\)).

This field is written only when the energy balance is active, because it is produced by the energy-balance first step and stored before the soil water balance runs — so the saved value is always a potential (Kc-adjusted) ET. When the energy balance is disabled (e.g. the raster forcing already provides pet_c or pet, or simulation.energy_balance = None), pet_c is not written to meteo_forcing.nc.

The actual ET state grid (output_states.evapotranspiration) is always saved as ET [m/s] and contains the actual ET as bounded by soil water availability (output of the soil water balance module).

Monthly Kc values

The default mapping is included with the package at mobidic/data/kc_clc_mapping.csv. The file contains one row per CLC class with 12 monthly \(K_c\) values (January–December):

clc_code,kc_jan,kc_feb,...,kc_dec
111,0.10,0.10,...,0.10
311,0.60,0.60,...,0.60
312,1.00,1.00,...,1.00
...

Comment lines starting with # are ignored. The default values are derived from the FAO-56 guidelines adapted to the CLC classification.

Configuration

Enabling the CLC raster

Set the path to the Corine Land Cover GeoTIFF under raster_files.CLC:

raster_files:
  CLC: example/raster/CLC_level3.tif   # OPTIONAL: CLC level 3 class codes

If the CLC raster is not provided, a uniform \(K_c\) equal to parameters.soil.Kc is applied across the whole basin (default 1.0).

Default Kc

parameters:
  soil:
    Kc: 1.0          # Scalar default Kc; used where CLC is absent or not in the mapping

Custom Kc/CLC mapping

To override the default mapping shipped with the package, point Kc_CLC_map to a user-provided CSV with the same column layout:

parameters:
  soil:
    Kc_CLC_map: custom/my_kc_mapping.csv   # OPTIONAL (blank = use built-in default)

The file must contain the columns clc_code, kc_jan, kc_feb, kc_mar, kc_apr, kc_may, kc_jun, kc_jul, kc_aug, kc_sep, kc_oct, kc_nov, kc_dec. CLC codes not present in the file fall back to parameters.soil.Kc.

Complete example

raster_files:
  CLC: geodat/CLC_level3.tif

parameters:
  soil:
    Kc: 1.0                           # Default for unclassified or missing codes
    Kc_CLC_map:                       # Leave blank to use the built-in default

simulation:
  energy_balance: 1L                  # Kc applied to CH when energy balance is active

Implementation notes

  • The Kc grid is rebuilt only when the month changes between timesteps (per-month cache).
  • CLC is an integer classification raster. During grid decimation it is sub-sampled using nearest-neighbour (upper-left sub-cell) rather than averaging, to preserve class codes.
  • The CLC grid is saved to and loaded from the consolidated gisdata.nc file (as the CLC variable) so that the mapping can be applied without the original GeoTIFF at simulation time.
  • When the CLC raster is present but a cell’s class code is not in the mapping, that cell gets parameters.soil.Kc.

Functions

mobidic.core.crop_coefficients.load_kc_clc_mapping(path=None)

Load a mapping from CLC codes to monthly Kc values.

The input CSV file may include comment lines starting with # and must contain a header row with the following columns:

clc_code, kc_jan, kc_feb, kc_mar, kc_apr, kc_may, kc_jun,
kc_jul, kc_aug, kc_sep, kc_oct, kc_nov, kc_dec

Parameters:

Name Type Description Default
path str | Path | None

Path to the CSV file. If None, the default mapping bundled with the package is used.

None

Returns:

Name Type Description
mapping dict[int, ndarray]

Dictionary mapping each CLC code (int) to a NumPy array of shape (12,) containing monthly Kc values ordered from January to December.

Raises:

Type Description
FileNotFoundError

If the provided file path does not exist.

ValueError

If the file format is invalid or required columns are missing.

Source code in mobidic/core/crop_coefficients.py
def load_kc_clc_mapping(path: str | Path | None = None) -> dict[int, np.ndarray]:
    """Load a mapping from CLC codes to monthly Kc values.

    The input CSV file may include comment lines starting with ``#`` and must
    contain a header row with the following columns:

        clc_code, kc_jan, kc_feb, kc_mar, kc_apr, kc_may, kc_jun,
        kc_jul, kc_aug, kc_sep, kc_oct, kc_nov, kc_dec

    Args:
        path: Path to the CSV file. If ``None``, the default mapping bundled
            with the package is used.

    Returns:
        mapping: Dictionary mapping each CLC code (int) to a NumPy array of shape
            ``(12,)`` containing monthly Kc values ordered from January to
            December.

    Raises:
        FileNotFoundError:
            If the provided file path does not exist.
        ValueError:
            If the file format is invalid or required columns are missing.
    """
    csv_path = Path(path) if path else default_kc_clc_mapping_path()
    if not csv_path.exists():
        raise FileNotFoundError(f"Kc/CLC mapping file not found: {csv_path}")

    df = pd.read_csv(csv_path, comment="#")
    expected = ["clc_code", *_MONTH_COLUMNS]
    missing = [c for c in expected if c not in df.columns]
    if missing:
        raise ValueError(f"Kc/CLC mapping file {csv_path} is missing columns: {missing}. Expected columns: {expected}")

    mapping: dict[int, np.ndarray] = {}
    for _, row in df.iterrows():
        code = int(row["clc_code"])
        values = np.asarray([row[c] for c in _MONTH_COLUMNS], dtype=float)
        mapping[code] = values

    logger.debug(f"Loaded Kc/CLC mapping from {csv_path}: {len(mapping)} classes")
    return mapping

mobidic.core.crop_coefficients.compute_kc_grid(clc_grid, mapping, month, default_kc)

Build a Kc grid for a given month from the CLC raster and mapping.

Cells whose CLC code is not found in the mapping fall back to default_kc. If clc_grid is None, the scalar default_kc is returned so that callers can multiply PET without allocating an extra grid.

Parameters:

Name Type Description Default
clc_grid ndarray | None

2D grid of CLC level 3 codes (NaN outside domain). May be None.

required
mapping dict[int, ndarray]

CLC code -> 12 monthly Kc values.

required
month int

Current month (1..12).

required
default_kc float

Kc value used where CLC is NaN or not present in the mapping.

required

Returns:

Name Type Description
kc ndarray | float

2D Kc grid with the same shape as clc_grid, or default_kc as a scalar when clc_grid is None.

Source code in mobidic/core/crop_coefficients.py
def compute_kc_grid(
    clc_grid: np.ndarray | None,
    mapping: dict[int, np.ndarray],
    month: int,
    default_kc: float,
) -> np.ndarray | float:
    """Build a Kc grid for a given month from the CLC raster and mapping.

    Cells whose CLC code is not found in the mapping fall back to ``default_kc``.
    If ``clc_grid`` is ``None``, the scalar ``default_kc`` is returned so that
    callers can multiply PET without allocating an extra grid.

    Args:
        clc_grid: 2D grid of CLC level 3 codes (NaN outside domain). May be None.
        mapping: CLC code -> 12 monthly Kc values.
        month: Current month (1..12).
        default_kc: Kc value used where CLC is NaN or not present in the mapping.

    Returns:
        kc: 2D Kc grid with the same shape as ``clc_grid``, or ``default_kc`` as a
            scalar when ``clc_grid`` is None.
    """
    if not 1 <= month <= 12:
        raise ValueError(f"month must be in 1..12, got {month}")

    if clc_grid is None:
        return float(default_kc)

    kc = np.full_like(clc_grid, default_kc, dtype=float)
    finite = np.isfinite(clc_grid)
    if not finite.any():
        return kc

    codes = np.rint(clc_grid[finite]).astype(np.int64)
    month_idx = month - 1
    unique_codes = np.unique(codes)
    kc_finite = np.full(codes.shape, default_kc, dtype=float)
    for code in unique_codes:
        values = mapping.get(int(code))
        if values is None:
            continue
        kc_finite[codes == code] = values[month_idx]

    kc[finite] = kc_finite
    return kc

mobidic.core.crop_coefficients.default_kc_clc_mapping_path()

Return the path to the CSV shipped with the package.

Source code in mobidic/core/crop_coefficients.py
def default_kc_clc_mapping_path() -> Path:
    """Return the path to the CSV shipped with the package."""
    return Path(str(files("mobidic.data").joinpath("kc_clc_mapping.csv")))