Skip to content

soil_params

Soil-specific parameters for the Lintul5 water balance.

Ports the constant soil-level inputs of the SIMPLACE WaterBalance.java component to a PyTorch-friendly dataclass.

Shapes

  • Scalar per batch: [B] or [] (broadcastable).
  • Multi-layer (optional, for future extensions): [B, N_layers].

The naming convention follows Lintul5: cSM* (volumetric soil moisture contents) drop the cSM prefix and become wc* (water content); other constants drop the c prefix.

References

  • Wolf, J. (2012). User guide for LINTUL5. Wageningen UR.
  • SIMPLACE source: simplace/sim/components/models/lintul5/WaterBalance.java.

SoilParameters dataclass

Soil hydraulic and water-balance parameters.

Note

Only a single-layer bucket model is implemented in the initial scaffold; the containers accept either [B] (single-layer) or [B, N_layers] tensors for future multi-layer extensions.

Source code in torchcrop/parameters/soil_params.py
@dataclass
class SoilParameters:
    """Soil hydraulic and water-balance parameters.

    Note:
        Only a single-layer bucket model is implemented in the initial
        scaffold; the containers accept either ``[B]`` (single-layer) or
        ``[B, N_layers]`` tensors for future multi-layer extensions.
    """

    # ------------------------------------------------------------------ #
    # 1. Volumetric soil-moisture contents [m³ m⁻³]
    # ------------------------------------------------------------------ #

    wcad: torch.Tensor = field(default_factory=lambda: _t(0.150))
    """``cSMDRY``. Volumetric soil-moisture content at air-dry
    [m³ m⁻³] (≈ pF 6.0). Lower bound of plant-available water."""

    wcwp: torch.Tensor = field(default_factory=lambda: _t(0.200))
    """``cSMW``. Volumetric soil-moisture content at the permanent
    wilting point [m³ m⁻³] (pF ≈ 4.2)."""

    wcfc: torch.Tensor = field(default_factory=lambda: _t(0.350))
    """``cSMFC``. Volumetric soil-moisture content at field capacity
    [m³ m⁻³] (pF ≈ 2.3) — drainage threshold."""

    wcst: torch.Tensor = field(default_factory=lambda: _t(0.450))
    """``cSM0``. Volumetric soil-moisture content at saturation
    [m³ m⁻³] — upper limit of pore-space water content."""

    wci: torch.Tensor = field(default_factory=lambda: _t(0.300))
    """``cSMI``. Initial volumetric soil-moisture content in the rooted
    zone [m³ m⁻³] at planting / emergence."""

    wci_lower: torch.Tensor = field(default_factory=lambda: _t(0.300))
    """``cSMLOWI``. Initial volumetric soil-moisture content in the
    **lower** zone (below the rooted zone) [m³ m⁻³]."""

    crairc: torch.Tensor = field(default_factory=lambda: _t(0.07))
    """``cCRAIRC``. Critical soil-air content [m³ m⁻³] for aeration —
    below this air-filled porosity the crop suffers waterlogging stress."""

    # ------------------------------------------------------------------ #
    # 2. Drainage, runoff, and percolation
    # ------------------------------------------------------------------ #

    ksub: torch.Tensor = field(default_factory=lambda: _t(10.0))
    """``cKSUB``. Maximum percolation rate [mm d⁻¹] from the lower zone
    to deeper soil layers (sub-surface drainage)."""

    runfr: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cRUNFR``. Average fraction [0–1] of incoming precipitation lost
    to surface runoff."""

    cfev: torch.Tensor = field(default_factory=lambda: _t(2.0))
    """``cCFEV``. Correction factor [-] for the time course of soil
    evaporation under drying conditions (Stroosnijder; range 1 – 4)."""

    # ------------------------------------------------------------------ #
    # 3. Rooting depth (soil-side)
    # ------------------------------------------------------------------ #

    rdmso: torch.Tensor = field(default_factory=lambda: _t(1.50))
    """``cRDMSO``. Maximum rooting depth [m] as constrained by the
    soil profile (e.g. by an impermeable layer). The actual maximum
    rooting depth is ``min(cRDMCR, cRDMSO)``."""

    # ------------------------------------------------------------------ #
    # 4. Irrigation
    # ------------------------------------------------------------------ #

    irri: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cIRRI``. Irrigation mode: ``0`` → no irrigation,
    ``1`` → automatic (refill to field capacity),
    ``2`` → use day-resolved `irrtab`."""

    irrtab: torch.Tensor | None = None
    """``cIRRTAB``. Optional table of effective irrigation applications
    [mm d⁻¹] as a function of day number; shape ``[N, 2]``."""

    scale_factor_irr: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorIRR``. Scale factor on `irrtab` y-values for
    sensitivity analysis / calibration."""

    # ------------------------------------------------------------------ #
    # 5. Soil mineral nutrient supply (mineralisation kinetics)
    # ------------------------------------------------------------------ #
    # The full Lintul5 NPK chain integrates two state pools per nutrient
    # (organic ``NMIN`` and inorganic ``NMINT``) updated daily by
    # ``SoilNutrients``. The constants below specify the *kinetics* of
    # that integration — the *current* pool levels live on `ModelState`.

    rtnmins: torch.Tensor = field(default_factory=lambda: _t(0.01))
    """``cRTNMINS``. Fraction [d⁻¹] of the soil organic-N pool that
    becomes mineralised and plant-available per day. Multiplied by the
    *initial* organic pool ``nmini`` (Lintul5 convention) and capped by
    the current ``NMIN``. Default 0.01 gives a steady-state daily
    mineralisation of ``0.01 × nmini`` g N m⁻² d⁻¹."""

    rtpmins: torch.Tensor = field(default_factory=lambda: _t(0.01))
    """``cRTPMINS``. Fraction [d⁻¹] of soil P becoming available per
    day. Default 0.01."""

    rtkmins: torch.Tensor = field(default_factory=lambda: _t(0.01))
    """``cRTKMINS``. Fraction [d⁻¹] of soil K becoming available per
    day. Default 0.01."""

    nmini: torch.Tensor = field(default_factory=lambda: _t(50.0))
    """``cNMINI``. Initial amount [g N m⁻²] of mineralisable soil
    organic N at crop emergence; this seeds `ModelState.nmin`. Default
    50 g/m² (~500 kg N/ha) combined with ``rtnmins=0.01`` produces a
    steady-state mineralisation flux of 0.5 g N m⁻² d⁻¹, matching the
    previous scalar surrogate."""

    pmini: torch.Tensor = field(default_factory=lambda: _t(5.0))
    """``cPMINI``. Initial mineralisable organic P pool [g P m⁻²].
    Default 5 g/m² × ``rtpmins=0.01`` ⇒ 0.05 g P m⁻² d⁻¹ steady-state
    supply."""

    kmini: torch.Tensor = field(default_factory=lambda: _t(30.0))
    """``cKMINI``. Initial mineralisable organic K pool [g K m⁻²].
    Default 30 g/m² × ``rtkmins=0.01`` ⇒ 0.30 g K m⁻² d⁻¹ steady-state
    supply."""

    nminti: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """Initial directly available inorganic N pool ``NMINT`` [g N m⁻²]
    at sowing/emergence. SIMPLACE default is 0; raise this to represent
    residual mineral N at planting."""

    pminti: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """Initial directly available inorganic P pool."""

    kminti: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """Initial directly available inorganic K pool."""

    # ------------------------------------------------------------------ #
    # Helpers
    # ------------------------------------------------------------------ #

    def validate(self) -> None:
        """Validate discrete/categorical soil fields.

        Checks that the irrigation mode ``irri`` holds only the
        supported discrete values ``{0, 1, 2}`` (per element when
        batched). The water-balance selection logic matches these modes
        by exact equality, so an off-grid value (e.g. ``1.5`` or ``3``)
        would silently fall through to "no irrigation"; this guard turns
        that into an explicit error.

        Raises:
            ValueError: If any ``irri`` element is not one of
                ``{0, 1, 2}``.
        """
        valid = torch.zeros_like(self.irri, dtype=torch.bool)
        for mode in (0.0, 1.0, 2.0):
            valid = valid | torch.isclose(
                self.irri, torch.full_like(self.irri, mode)
            )
        if not bool(valid.all()):
            raise ValueError(
                "soil_params.irri must be one of {0, 1, 2} (0=none, "
                f"1=auto refill, 2=irrtab); got {self.irri.tolist()}"
            )

    def to(
        self,
        dtype: torch.dtype | None = None,
        device: torch.device | str | None = None,
    ) -> "SoilParameters":
        """Cast and/or move all tensor fields to a new dtype/device.

        Args:
            dtype: Target tensor dtype, or ``None`` to leave unchanged.
            device: Target torch device, or ``None`` to leave unchanged.

        Returns:
            A new `SoilParameters` with every tensor field moved/cast.
        """
        kwargs: dict[str, Any] = {}
        for f in fields(self):
            t = getattr(self, f.name)
            if isinstance(t, torch.Tensor):
                kwargs[f.name] = t.to(dtype=dtype, device=device)
            else:
                kwargs[f.name] = t
        return SoilParameters(**kwargs)

cfev: Tensor dataclass-field

cCFEV. Correction factor [-] for the time course of soil evaporation under drying conditions (Stroosnijder; range 1 – 4).

crairc: Tensor dataclass-field

cCRAIRC. Critical soil-air content [m³ m⁻³] for aeration — below this air-filled porosity the crop suffers waterlogging stress.

irri: Tensor dataclass-field

cIRRI. Irrigation mode: 0 → no irrigation, 1 → automatic (refill to field capacity), 2 → use day-resolved irrtab.

irrtab: torch.Tensor | None dataclass-field

cIRRTAB. Optional table of effective irrigation applications [mm d⁻¹] as a function of day number; shape [N, 2].

kmini: Tensor dataclass-field

cKMINI. Initial mineralisable organic K pool [g K m⁻²]. Default 30 g/m² × rtkmins=0.01 ⇒ 0.30 g K m⁻² d⁻¹ steady-state supply.

kminti: Tensor dataclass-field

Initial directly available inorganic K pool.

ksub: Tensor dataclass-field

cKSUB. Maximum percolation rate [mm d⁻¹] from the lower zone to deeper soil layers (sub-surface drainage).

nmini: Tensor dataclass-field

cNMINI. Initial amount [g N m⁻²] of mineralisable soil organic N at crop emergence; this seeds ModelState.nmin. Default 50 g/m² (~500 kg N/ha) combined with rtnmins=0.01 produces a steady-state mineralisation flux of 0.5 g N m⁻² d⁻¹, matching the previous scalar surrogate.

nminti: Tensor dataclass-field

Initial directly available inorganic N pool NMINT [g N m⁻²] at sowing/emergence. SIMPLACE default is 0; raise this to represent residual mineral N at planting.

pmini: Tensor dataclass-field

cPMINI. Initial mineralisable organic P pool [g P m⁻²]. Default 5 g/m² × rtpmins=0.01 ⇒ 0.05 g P m⁻² d⁻¹ steady-state supply.

pminti: Tensor dataclass-field

Initial directly available inorganic P pool.

rdmso: Tensor dataclass-field

cRDMSO. Maximum rooting depth [m] as constrained by the soil profile (e.g. by an impermeable layer). The actual maximum rooting depth is min(cRDMCR, cRDMSO).

rtkmins: Tensor dataclass-field

cRTKMINS. Fraction [d⁻¹] of soil K becoming available per day. Default 0.01.

rtnmins: Tensor dataclass-field

cRTNMINS. Fraction [d⁻¹] of the soil organic-N pool that becomes mineralised and plant-available per day. Multiplied by the initial organic pool nmini (Lintul5 convention) and capped by the current NMIN. Default 0.01 gives a steady-state daily mineralisation of 0.01 × nmini g N m⁻² d⁻¹.

rtpmins: Tensor dataclass-field

cRTPMINS. Fraction [d⁻¹] of soil P becoming available per day. Default 0.01.

runfr: Tensor dataclass-field

cRUNFR. Average fraction [0–1] of incoming precipitation lost to surface runoff.

scale_factor_irr: Tensor dataclass-field

cScaleFactorIRR. Scale factor on irrtab y-values for sensitivity analysis / calibration.

wcad: Tensor dataclass-field

cSMDRY. Volumetric soil-moisture content at air-dry [m³ m⁻³] (≈ pF 6.0). Lower bound of plant-available water.

wcfc: Tensor dataclass-field

cSMFC. Volumetric soil-moisture content at field capacity [m³ m⁻³] (pF ≈ 2.3) — drainage threshold.

wci: Tensor dataclass-field

cSMI. Initial volumetric soil-moisture content in the rooted zone [m³ m⁻³] at planting / emergence.

wci_lower: Tensor dataclass-field

cSMLOWI. Initial volumetric soil-moisture content in the lower zone (below the rooted zone) [m³ m⁻³].

wcst: Tensor dataclass-field

cSM0. Volumetric soil-moisture content at saturation [m³ m⁻³] — upper limit of pore-space water content.

wcwp: Tensor dataclass-field

cSMW. Volumetric soil-moisture content at the permanent wilting point [m³ m⁻³] (pF ≈ 4.2).

to(self, dtype=None, device=None)

Cast and/or move all tensor fields to a new dtype/device.

Parameters:

Name Type Description Default
dtype torch.dtype | None

Target tensor dtype, or None to leave unchanged.

None
device torch.device | str | None

Target torch device, or None to leave unchanged.

None

Returns:

Type Description
'SoilParameters'

A new SoilParameters with every tensor field moved/cast.

Source code in torchcrop/parameters/soil_params.py
def to(
    self,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> "SoilParameters":
    """Cast and/or move all tensor fields to a new dtype/device.

    Args:
        dtype: Target tensor dtype, or ``None`` to leave unchanged.
        device: Target torch device, or ``None`` to leave unchanged.

    Returns:
        A new `SoilParameters` with every tensor field moved/cast.
    """
    kwargs: dict[str, Any] = {}
    for f in fields(self):
        t = getattr(self, f.name)
        if isinstance(t, torch.Tensor):
            kwargs[f.name] = t.to(dtype=dtype, device=device)
        else:
            kwargs[f.name] = t
    return SoilParameters(**kwargs)

validate(self)

Validate discrete/categorical soil fields.

Checks that the irrigation mode irri holds only the supported discrete values {0, 1, 2} (per element when batched). The water-balance selection logic matches these modes by exact equality, so an off-grid value (e.g. 1.5 or 3) would silently fall through to "no irrigation"; this guard turns that into an explicit error.

Exceptions:

Type Description
ValueError

If any irri element is not one of {0, 1, 2}.

Source code in torchcrop/parameters/soil_params.py
def validate(self) -> None:
    """Validate discrete/categorical soil fields.

    Checks that the irrigation mode ``irri`` holds only the
    supported discrete values ``{0, 1, 2}`` (per element when
    batched). The water-balance selection logic matches these modes
    by exact equality, so an off-grid value (e.g. ``1.5`` or ``3``)
    would silently fall through to "no irrigation"; this guard turns
    that into an explicit error.

    Raises:
        ValueError: If any ``irri`` element is not one of
            ``{0, 1, 2}``.
    """
    valid = torch.zeros_like(self.irri, dtype=torch.bool)
    for mode in (0.0, 1.0, 2.0):
        valid = valid | torch.isclose(
            self.irri, torch.full_like(self.irri, mode)
        )
    if not bool(valid.all()):
        raise ValueError(
            "soil_params.irri must be one of {0, 1, 2} (0=none, "
            f"1=auto refill, 2=irrtab); got {self.irri.tolist()}"
        )

default_loam_params(dtype=torch.float32)

Return the SIMPLACE Lintul5 default loam-like soil parameter set.

Parameters:

Name Type Description Default
dtype torch.dtype

Target tensor dtype for all scalar fields.

torch.float32

Returns:

Type Description
SoilParameters

A fresh SoilParameters with the Lintul5 loam defaults cast to dtype.

Source code in torchcrop/parameters/soil_params.py
def default_loam_params(dtype: torch.dtype = torch.float32) -> SoilParameters:
    """Return the SIMPLACE Lintul5 default loam-like soil parameter set.

    Args:
        dtype: Target tensor dtype for all scalar fields.

    Returns:
        A fresh `SoilParameters` with the Lintul5 loam defaults
        cast to ``dtype``.
    """
    return SoilParameters().to(dtype=dtype)