Skip to content

nutrient_demand

Crop nutrient demand, uptake, translocation and stress indices.

References

NPK block of Lintul5.java. A minimal, batch-compatible formulation is implemented: daily demand from maximum concentrations, supply from the soil reservoir, and a simple nutrient-stress index tied to leaf N.

NutrientDemand (Module)

Daily NPK demand, uptake and stress indices.

Source code in torchcrop/processes/nutrient_demand.py
class NutrientDemand(nn.Module):
    """Daily NPK demand, uptake and stress indices."""

    def forward(
        self,
        state: ModelState,
        g_lv: torch.Tensor,
        g_st: torch.Tensor,
        g_rt: torch.Tensor,
        g_so: torch.Tensor,
        crop_params: CropParameters,
        soil_params: SoilParameters,
    ) -> dict[str, torch.Tensor]:
        """Compute daily NPK uptake per organ and the nutrient-stress factor.

        Args:
            state: Current state (unused in this minimal port but kept for
                the uniform process signature).
            g_lv: Leaf biomass growth rate [g DM m⁻² d⁻¹], shape ``[B]``,
                from `Partitioning`.
            g_st: Stem biomass growth rate, shape ``[B]``.
            g_rt: Root biomass growth rate, shape ``[B]``.
            g_so: Storage-organ biomass growth rate, shape ``[B]``.
            crop_params: Crop parameters; uses maximum tissue concentrations
                ``{n,p,k}max{lv,st,rt,so}``.
            soil_params: Soil parameters; uses available soil pools
                ``nmins``, ``pmins``, ``kmins``.

        Returns:
            Dict of ``[B]`` tensors grouped as follows.

            Rate variables (per-organ NPK uptake — consumed by the engine
            to update the corresponding ``a{n,p,k}{lv,st,rt,so}`` state
            pools):

                * ``n_lv_rate``, ``n_st_rate``, ``n_rt_rate``,
                  ``n_so_rate`` [g N m⁻² d⁻¹] — Daily nitrogen uptake by
                  leaves, stems, roots, storage organs.
                * ``p_lv_rate``, ``p_st_rate``, ``p_rt_rate``,
                  ``p_so_rate`` [g P m⁻² d⁻¹] — Daily phosphorus uptake
                  per organ.
                * ``k_lv_rate``, ``k_st_rate``, ``k_rt_rate``,
                  ``k_so_rate`` [g K m⁻² d⁻¹] — Daily potassium uptake
                  per organ.

            Diagnostics:

                * ``n_uptake``, ``p_uptake``, ``k_uptake`` [g X m⁻² d⁻¹] —
                  Whole-plant uptake totals before per-organ splitting.
                * ``nstress`` [-] — Combined nutrient-stress factor in
                  ``[0, 1] = min(uptake/demand)`` across N, P, K (1 when
                  there is no demand). Multiplies ``gtotal`` in
                  `Photosynthesis`.
        """
        # Maximum-demand uptake per organ (fraction of DM growth)
        n_demand = (
            g_lv * crop_params.nmaxlv
            + g_st * crop_params.nmaxst
            + g_rt * crop_params.nmaxrt
            + g_so * crop_params.nmaxso
        )
        p_demand = (
            g_lv * crop_params.pmaxlv
            + g_st * crop_params.pmaxst
            + g_rt * crop_params.pmaxrt
            + g_so * crop_params.pmaxso
        )
        k_demand = (
            g_lv * crop_params.kmaxlv
            + g_st * crop_params.kmaxst
            + g_rt * crop_params.kmaxrt
            + g_so * crop_params.kmaxso
        )

        n_uptake = torch.minimum(n_demand, soil_params.nmins.expand_as(n_demand))
        p_uptake = torch.minimum(p_demand, soil_params.pmins.expand_as(p_demand))
        k_uptake = torch.minimum(k_demand, soil_params.kmins.expand_as(k_demand))

        # Per-organ allocation proportional to per-organ demand shares
        def split(uptake: torch.Tensor, shares: list[torch.Tensor]) -> list[torch.Tensor]:
            total = sum(shares)
            total = torch.where(total > 1e-10, total, torch.ones_like(total))
            return [uptake * s / total for s in shares]

        n_lv, n_st, n_rt, n_so = split(
            n_uptake,
            [
                g_lv * crop_params.nmaxlv,
                g_st * crop_params.nmaxst,
                g_rt * crop_params.nmaxrt,
                g_so * crop_params.nmaxso,
            ],
        )
        p_lv, p_st, p_rt, p_so = split(
            p_uptake,
            [
                g_lv * crop_params.pmaxlv,
                g_st * crop_params.pmaxst,
                g_rt * crop_params.pmaxrt,
                g_so * crop_params.pmaxso,
            ],
        )
        k_lv, k_st, k_rt, k_so = split(
            k_uptake,
            [
                g_lv * crop_params.kmaxlv,
                g_st * crop_params.kmaxst,
                g_rt * crop_params.kmaxrt,
                g_so * crop_params.kmaxso,
            ],
        )

        # Stress index = uptake / demand (min across N, P, K)
        def ratio(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
            return torch.clamp(a / torch.where(b > 1e-10, b, torch.ones_like(b)), 0.0, 1.0)

        nstress = torch.minimum(
            torch.minimum(ratio(n_uptake, n_demand), ratio(p_uptake, p_demand)),
            ratio(k_uptake, k_demand),
        )
        # When there is no demand (e.g. DVS=0, no growth), stress = 1 (no stress).
        no_demand = ((n_demand + p_demand + k_demand) < 1e-10).to(nstress.dtype)
        nstress = nstress * (1.0 - no_demand) + no_demand

        return {
            "nstress": nstress,
            "n_uptake": n_uptake,
            "p_uptake": p_uptake,
            "k_uptake": k_uptake,
            "n_lv_rate": n_lv,
            "n_st_rate": n_st,
            "n_rt_rate": n_rt,
            "n_so_rate": n_so,
            "p_lv_rate": p_lv,
            "p_st_rate": p_st,
            "p_rt_rate": p_rt,
            "p_so_rate": p_so,
            "k_lv_rate": k_lv,
            "k_st_rate": k_st,
            "k_rt_rate": k_rt,
            "k_so_rate": k_so,
        }

forward(self, state, g_lv, g_st, g_rt, g_so, crop_params, soil_params)

Compute daily NPK uptake per organ and the nutrient-stress factor.

Parameters:

Name Type Description Default
state ModelState

Current state (unused in this minimal port but kept for the uniform process signature).

required
g_lv torch.Tensor

Leaf biomass growth rate [g DM m⁻² d⁻¹], shape [B], from Partitioning.

required
g_st torch.Tensor

Stem biomass growth rate, shape [B].

required
g_rt torch.Tensor

Root biomass growth rate, shape [B].

required
g_so torch.Tensor

Storage-organ biomass growth rate, shape [B].

required
crop_params CropParameters

Crop parameters; uses maximum tissue concentrations {n,p,k}max{lv,st,rt,so}.

required
soil_params SoilParameters

Soil parameters; uses available soil pools nmins, pmins, kmins.

required

Returns:

Type Description
Dict of ``[B]`` tensors grouped as follows. Rate variables (per-organ NPK uptake — consumed by the engine to update the corresponding ``a{n,p,k}{lv,st,rt,so}`` state pools)
  • n_lv_rate, n_st_rate, n_rt_rate, n_so_rate [g N m⁻² d⁻¹] — Daily nitrogen uptake by leaves, stems, roots, storage organs.
    • p_lv_rate, p_st_rate, p_rt_rate, p_so_rate [g P m⁻² d⁻¹] — Daily phosphorus uptake per organ.
    • k_lv_rate, k_st_rate, k_rt_rate, k_so_rate [g K m⁻² d⁻¹] — Daily potassium uptake per organ.

Diagnostics:

1
2
3
4
5
6
* ``n_uptake``, ``p_uptake``, ``k_uptake`` [g X m⁻² d⁻¹] —
  Whole-plant uptake totals before per-organ splitting.
* ``nstress`` [-] — Combined nutrient-stress factor in
  ``[0, 1] = min(uptake/demand)`` across N, P, K (1 when
  there is no demand). Multiplies ``gtotal`` in
  `Photosynthesis`.
Source code in torchcrop/processes/nutrient_demand.py
def forward(
    self,
    state: ModelState,
    g_lv: torch.Tensor,
    g_st: torch.Tensor,
    g_rt: torch.Tensor,
    g_so: torch.Tensor,
    crop_params: CropParameters,
    soil_params: SoilParameters,
) -> dict[str, torch.Tensor]:
    """Compute daily NPK uptake per organ and the nutrient-stress factor.

    Args:
        state: Current state (unused in this minimal port but kept for
            the uniform process signature).
        g_lv: Leaf biomass growth rate [g DM m⁻² d⁻¹], shape ``[B]``,
            from `Partitioning`.
        g_st: Stem biomass growth rate, shape ``[B]``.
        g_rt: Root biomass growth rate, shape ``[B]``.
        g_so: Storage-organ biomass growth rate, shape ``[B]``.
        crop_params: Crop parameters; uses maximum tissue concentrations
            ``{n,p,k}max{lv,st,rt,so}``.
        soil_params: Soil parameters; uses available soil pools
            ``nmins``, ``pmins``, ``kmins``.

    Returns:
        Dict of ``[B]`` tensors grouped as follows.

        Rate variables (per-organ NPK uptake — consumed by the engine
        to update the corresponding ``a{n,p,k}{lv,st,rt,so}`` state
        pools):

            * ``n_lv_rate``, ``n_st_rate``, ``n_rt_rate``,
              ``n_so_rate`` [g N m⁻² d⁻¹] — Daily nitrogen uptake by
              leaves, stems, roots, storage organs.
            * ``p_lv_rate``, ``p_st_rate``, ``p_rt_rate``,
              ``p_so_rate`` [g P m⁻² d⁻¹] — Daily phosphorus uptake
              per organ.
            * ``k_lv_rate``, ``k_st_rate``, ``k_rt_rate``,
              ``k_so_rate`` [g K m⁻² d⁻¹] — Daily potassium uptake
              per organ.

        Diagnostics:

            * ``n_uptake``, ``p_uptake``, ``k_uptake`` [g X m⁻² d⁻¹] —
              Whole-plant uptake totals before per-organ splitting.
            * ``nstress`` [-] — Combined nutrient-stress factor in
              ``[0, 1] = min(uptake/demand)`` across N, P, K (1 when
              there is no demand). Multiplies ``gtotal`` in
              `Photosynthesis`.
    """
    # Maximum-demand uptake per organ (fraction of DM growth)
    n_demand = (
        g_lv * crop_params.nmaxlv
        + g_st * crop_params.nmaxst
        + g_rt * crop_params.nmaxrt
        + g_so * crop_params.nmaxso
    )
    p_demand = (
        g_lv * crop_params.pmaxlv
        + g_st * crop_params.pmaxst
        + g_rt * crop_params.pmaxrt
        + g_so * crop_params.pmaxso
    )
    k_demand = (
        g_lv * crop_params.kmaxlv
        + g_st * crop_params.kmaxst
        + g_rt * crop_params.kmaxrt
        + g_so * crop_params.kmaxso
    )

    n_uptake = torch.minimum(n_demand, soil_params.nmins.expand_as(n_demand))
    p_uptake = torch.minimum(p_demand, soil_params.pmins.expand_as(p_demand))
    k_uptake = torch.minimum(k_demand, soil_params.kmins.expand_as(k_demand))

    # Per-organ allocation proportional to per-organ demand shares
    def split(uptake: torch.Tensor, shares: list[torch.Tensor]) -> list[torch.Tensor]:
        total = sum(shares)
        total = torch.where(total > 1e-10, total, torch.ones_like(total))
        return [uptake * s / total for s in shares]

    n_lv, n_st, n_rt, n_so = split(
        n_uptake,
        [
            g_lv * crop_params.nmaxlv,
            g_st * crop_params.nmaxst,
            g_rt * crop_params.nmaxrt,
            g_so * crop_params.nmaxso,
        ],
    )
    p_lv, p_st, p_rt, p_so = split(
        p_uptake,
        [
            g_lv * crop_params.pmaxlv,
            g_st * crop_params.pmaxst,
            g_rt * crop_params.pmaxrt,
            g_so * crop_params.pmaxso,
        ],
    )
    k_lv, k_st, k_rt, k_so = split(
        k_uptake,
        [
            g_lv * crop_params.kmaxlv,
            g_st * crop_params.kmaxst,
            g_rt * crop_params.kmaxrt,
            g_so * crop_params.kmaxso,
        ],
    )

    # Stress index = uptake / demand (min across N, P, K)
    def ratio(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
        return torch.clamp(a / torch.where(b > 1e-10, b, torch.ones_like(b)), 0.0, 1.0)

    nstress = torch.minimum(
        torch.minimum(ratio(n_uptake, n_demand), ratio(p_uptake, p_demand)),
        ratio(k_uptake, k_demand),
    )
    # When there is no demand (e.g. DVS=0, no growth), stress = 1 (no stress).
    no_demand = ((n_demand + p_demand + k_demand) < 1e-10).to(nstress.dtype)
    nstress = nstress * (1.0 - no_demand) + no_demand

    return {
        "nstress": nstress,
        "n_uptake": n_uptake,
        "p_uptake": p_uptake,
        "k_uptake": k_uptake,
        "n_lv_rate": n_lv,
        "n_st_rate": n_st,
        "n_rt_rate": n_rt,
        "n_so_rate": n_so,
        "p_lv_rate": p_lv,
        "p_st_rate": p_st,
        "p_rt_rate": p_rt,
        "p_so_rate": p_so,
        "k_lv_rate": k_lv,
        "k_st_rate": k_st,
        "k_rt_rate": k_rt,
        "k_so_rate": k_so,
    }