Skip to content

crop_params

Crop-specific parameters for Lintul5.

This module ports the constant crop-level inputs of the SIMPLACE Lintul5 component family (Lintul5.java, Phenology.java, RadiationUseEfficiency.java) to a single PyTorch-friendly dataclass.

All scalar fields are stored as torch.Tensor so they can be made learnable by wrapping them with torch.nn.Parameter. Table fields (..._tb / ..._table) carry shape [N, 2] (or [B, N, 2] if the table itself is batch-varying), where column 0 is the abscissa (DVS or temperature) and column 1 is the value. Tables are interpolated by torchcrop.functions.interpolate.

Naming conventions: * Field names follow the original Lintul5 (Wolf, 2012) symbol, lowercased. * Constants prefixed with c in SIMPLACE drop the c prefix here. * Tables originally named cXxxTableY are concatenated into a single two-column tensor named xxx_tb (or xxx_table).

References

Wolf, J. (2012). User guide for LINTUL5. Wageningen UR.

CropParameters dataclass

Species-specific Lintul5 crop parameters.

Scalar fields have shape [] or [B] (broadcastable against the batch dimension). Table fields have shape [N, 2] or [B, N, 2].

Parameters are loaded from a YAML preset on construction:

1
2
3
4
params = CropParameters()                       # built-in default.yaml
params = CropParameters(crop_name="wheat")      # built-in by name
params = CropParameters(crop_name="maize")
params = CropParameters(config_file="my_crop.yaml")  # custom file

crop_name selects a bundled preset from parameters/crop_data/ and is matched case- and whitespace-insensitively ("grain maize"maize); see available_crops for the full list. config_file loads a user-provided YAML, making it easy to add custom parameterisations. With neither argument, the built-in default preset is loaded. crop_name and config_file are mutually exclusive.

Two preset layouts are accepted (see _apply_preset): the sectioned layout the bundled presets use — a sections mapping of thematic groups (Development, Water Use, Nutrient (N-P-K) Use, Root Growth, Stress Response, …), each with its own scalars and/or tables — and the legacy flat layout with top-level scalars and tables mappings. Both key on field names.

A preset overwrites every field it defines; any field it omits keeps the class default (e.g. torchcrop-specific extensions not present in the SIMPLACE source). Customise individual values afterwards (params.scale_factor_rue = ...) or use from_crop_name for dtype control.

Source code in torchcrop/parameters/crop_params.py
@dataclass
class CropParameters:
    """Species-specific Lintul5 crop parameters.

    Scalar fields have shape ``[]`` or ``[B]`` (broadcastable against the
    batch dimension). Table fields have shape ``[N, 2]`` or ``[B, N, 2]``.

    Parameters are loaded from a YAML preset on construction:

        params = CropParameters()                       # built-in default.yaml
        params = CropParameters(crop_name="wheat")      # built-in by name
        params = CropParameters(crop_name="maize")
        params = CropParameters(config_file="my_crop.yaml")  # custom file

    ``crop_name`` selects a bundled preset from ``parameters/crop_data/`` and
    is matched case- and whitespace-insensitively (``"grain maize"`` →
    ``maize``); see `available_crops` for the full list. ``config_file``
    loads a user-provided YAML, making it easy to add custom
    parameterisations. With neither argument, the built-in ``default`` preset
    is loaded. ``crop_name`` and ``config_file`` are mutually exclusive.

    Two preset layouts are accepted (see `_apply_preset`): the **sectioned**
    layout the bundled presets use — a ``sections`` mapping of thematic
    groups (``Development``, ``Water Use``, ``Nutrient (N-P-K) Use``,
    ``Root Growth``, ``Stress Response``, …), each with its own ``scalars``
    and/or ``tables`` — and the legacy **flat** layout with top-level
    ``scalars`` and ``tables`` mappings. Both key on field names.

    A preset overwrites every field it defines; any field it omits keeps the
    class default (e.g. torchcrop-specific extensions not present in the
    SIMPLACE source). Customise individual values afterwards
    (``params.scale_factor_rue = ...``) or use `from_crop_name` for dtype
    control.
    """

    # ------------------------------------------------------------------ #
    # 1. Phenology (from Phenology.java)
    # ------------------------------------------------------------------ #

    idsl: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cIDSL``. Phenology mode selector:
    ``0`` → temperature only; ``1`` → temperature + day length;
    ``2`` → temperature + day length + vernalisation. Stored as float so it
    can be used in differentiable masking; cast to int for branching."""

    tbasem: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cTBASEM``. Lower threshold (base) temperature [°C] for
    accumulation of temperature sum **before** crop emergence."""

    teffmx: torch.Tensor = field(default_factory=lambda: _t(30.0))
    """``cTEFFMX``. Maximum effective temperature [°C] used when
    accumulating temperature sum **before** emergence (caps TEFF)."""

    tsumem: torch.Tensor = field(default_factory=lambda: _t(110.0))
    """``cTSUMEM``. Required temperature sum [°C·d] from sowing to
    emergence."""

    tsum1: torch.Tensor = field(default_factory=lambda: _t(900.0))
    """``cTSUM1``. Required temperature sum [°C·d] from emergence to
    flowering / anthesis (vegetative period, DVS 0 → 1)."""

    tsum2: torch.Tensor = field(default_factory=lambda: _t(700.0))
    """``cTSUM2``. Required temperature sum [°C·d] from anthesis to
    maturity (generative period, DVS 1 → 2)."""

    dvsi: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cDVSI``. Initial development stage of the crop at the start of
    the simulation (in the range ``0`` … ``2``)."""

    dtsmtb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(-5.0, 0.0), (0.0, 0.0), (30.0, 30.0), (45.0, 30.0)]
        )
    )
    """``cDTSMTB`` (= ``cTsumIncrementTableMeanTemp`` × ``cTsumIncrementTableRate``).
    Daily increment of temperature sum [°C·d] as a function of mean daily
    air temperature [°C]. Shape ``[N, 2]`` with x = T̄, y = ΔTsum/d."""

    phottb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.0), (8.0, 1.0), (12.0, 1.0), (18.0, 1.0)]
        )
    )
    """``cPHOTTB`` (= ``cPhotoperiodTableHour`` × ``cPhotoperiodTableFactor``).
    Photoperiod reduction factor [-] of development rate (until anthesis)
    as a function of day length [h]."""

    vernrt: torch.Tensor = field(
        default_factory=lambda: _table([(0.0, 1.0), (1.0, 1.0)])
    )
    """``cVERNRT`` (= ``cVernalisationTableMeanTemp`` × ``cVernalisationTableRate``).
    Daily vernal-day rate [-] as a function of mean daily temperature [°C].
    Used only when `idsl` ≥ 2."""

    vbase: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cVBASE``. Vernalisation base [thermal day]: vernal days
    accumulated below this value contribute nothing to the vernalisation
    factor."""

    versat: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cVERSAT``. Vernalisation saturation [thermal day]: number of
    vernal days above which the vernalisation factor is fully released
    (=1)."""

    vernalisation_devstage: torch.Tensor = field(default_factory=lambda: _t(0.3))
    """``cVernalisationDevStage``. Maximum DVS [-] up to which the
    vernalisation factor is applied; beyond this stage VERNFAC = 1."""

    minimal_vernalisation_factor: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cMinimalVernalisationFactor``. Lower bound [0–1] on the
    vernalisation factor used to limit pheno-rate suppression."""

    # ------------------------------------------------------------------ #
    # 2. Radiation use efficiency / RUE (from RadiationUseEfficiency.java)
    # ------------------------------------------------------------------ #

    day_temp_factor: torch.Tensor = field(default_factory=lambda: _t(0.25))
    """``cDayTempFactor``. Weight ``f`` in
    ``T_day = TMAX − f·(TMAX − TMIN)`` used to derive a *daytime* mean
    temperature from min/max. ``f = 0.25`` → daytime mean (default);
    ``f = 0.5`` → 24-h mean."""

    ruetb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 3.0), (1.0, 3.0), (1.3, 3.0), (2.0, 0.4)]
        )
    )
    """``cRUETB`` (= ``cRUETableDVS`` × ``cRUETableRUE``). Radiation use
    efficiency [g DM · MJ⁻¹ PAR] as a function of DVS (declines after
    grain filling)."""

    tmpftb: torch.Tensor = field(
        default_factory=lambda: _table(
            [
                (-1.0, 0.0),
                (0.0, 0.0),
                (10.0, 0.6),
                (15.0, 1.0),
                (30.0, 1.0),
                (35.0, 0.0),
                (40.0, 0.0),
            ]
        )
    )
    """``cTMPFTB``. RUE reduction factor [-] as a function of mean daytime
    temperature [°C] (high- and low-temperature stress)."""

    tmnftb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(-5.0, 0.0), (0.0, 0.0), (3.0, 1.0), (30.0, 1.0)]
        )
    )
    """``cTMNFTB``. RUE reduction factor [-] as a function of daily
    minimum temperature [°C] (cold-night stress)."""

    cotb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(40.0, 0.0), (360.0, 1.0), (720.0, 1.35), (1000.0, 1.50), (2000.0, 1.50)]
        )
    )
    """``cCOTB``. CO₂ correction factor [-] applied to RUE as a function of
    atmospheric CO₂ concentration [ppm]."""

    scale_factor_rue: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorRUE``. Multiplicative scale factor on the y-values of
    `ruetb` for sensitivity analysis / calibration."""

    # ------------------------------------------------------------------ #
    # 3. Light interception / canopy (from Lintul5.java)
    # ------------------------------------------------------------------ #

    kdiftb: torch.Tensor = field(
        default_factory=lambda: _table([(0.0, 0.6), (2.0, 0.6)])
    )
    """``cKDIFTB``. Extinction coefficient [-] for diffuse PAR as a
    function of DVS."""

    scale_factor_kdif: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorKDIF``. Scale factor on `kdiftb` y-values."""

    laicr: torch.Tensor = field(default_factory=lambda: _t(4.0))
    """``cLAICR``. Critical leaf area index [m² m⁻²] above which leaves
    suffer self-shading mortality."""

    # ------------------------------------------------------------------ #
    # 3b. Crop water-use response (from WaterBalance.java)
    # ------------------------------------------------------------------ #
    # ``cDEPNR`` and ``cIAIRDU`` are consumed by the water balance but are
    # *crop* traits in SIMPLACE — they live in the crop data file
    # (``simplace/crop_default/*.xml``, "Water Use" group) and vary by
    # species (e.g. rice has ``IAIRDU = 1`` and ``DEPNR = 3.5``), so they
    # belong here rather than on `SoilParameters`. The water balance reads
    # them through `Lintul5Model`, which forwards these crop fields.

    depnr: torch.Tensor = field(default_factory=lambda: _t(4.5))
    """``cDEPNR``. Crop group number [-] for soil-water depletion
    (Doorenbos & Kassam). Used to compute the critical soil-moisture
    content above which transpiration is unrestricted."""

    iairdu: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cIAIRDU``. Boolean flag (0/1) indicating whether the crop has
    air ducts in its roots (=1, e.g. rice → tolerates waterlogging) or
    not (=0). Stored as float for batch broadcasting."""

    cfet: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cCFET``. Crop-specific empirical correction factor [-] applied to
    the Penman transpiration rate (canopy-resistance proxy)."""

    # ------------------------------------------------------------------ #
    # 4. Initial biomass and rooting (from Lintul5.java)
    # ------------------------------------------------------------------ #

    tdwi: torch.Tensor = field(default_factory=lambda: _t(210.0))
    """``cTDWI``. Initial total crop dry weight [g DM m⁻²] at sowing /
    emergence; partitioned to organs via the partitioning tables."""

    rdi: torch.Tensor = field(default_factory=lambda: _t(0.10))
    """``cRDI``. Initial rooting depth [m] (also used by WaterBalance)."""

    rri: torch.Tensor = field(default_factory=lambda: _t(0.012))
    """``cRRI``. Maximum daily increase in rooting depth [m d⁻¹]."""

    rdmcr: torch.Tensor = field(default_factory=lambda: _t(1.20))
    """``cRDMCR``. Crop-specific maximum rooting depth [m]."""

    # ------------------------------------------------------------------ #
    # 5. Leaf dynamics & senescence (from Lintul5.java)
    # ------------------------------------------------------------------ #

    slatb: torch.Tensor = field(
        default_factory=lambda: _table([(0.0, 0.0212), (2.0, 0.0212)])
    )
    """``cSLATB``. Specific leaf area [m² g⁻¹] as a function of DVS."""

    scale_factor_sla: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorSLA``. Scale factor on `slatb` y-values."""

    laii: torch.Tensor = field(default_factory=lambda: _t(0.012))
    """``LAII`` initial leaf area index [m² m⁻²] at emergence (Lintul5
    output, persisted here as a parameter for re-init)."""

    rgrl: torch.Tensor = field(default_factory=lambda: _t(0.009))
    """``cRGRLAI``. Maximum relative increase in LAI [d⁻¹] during the
    juvenile (exponential) phase."""

    tbase: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cTBASE``. Lower threshold temperature [°C] for LAI increase."""

    rdrshm: torch.Tensor = field(default_factory=lambda: _t(0.03))
    """``cRDRSHM``. Maximum relative death rate of leaves [d⁻¹] caused by
    shading when LAI > `laicr`."""

    rdrl: torch.Tensor = field(default_factory=lambda: _t(0.05))
    """``cRDRL``. Maximum relative death rate of leaves [d⁻¹] due to
    water stress."""

    rdrns: torch.Tensor = field(default_factory=lambda: _t(0.05))
    """``cRDRNS``. Maximum relative death rate of leaves [d⁻¹] due to
    NPK (nutrient) stress."""

    rdrltb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(-10.0, 0.0), (10.0, 0.02), (15.0, 0.03), (30.0, 0.05), (50.0, 0.09)]
        )
    )
    """``cRDRLTB``. Relative death rate of leaves [d⁻¹] as a function of
    mean daily temperature [°C] (heat-stress curve)."""

    rdrrtb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.0), (1.5, 0.0), (1.5001, 0.02), (2.0, 0.02)]
        )
    )
    """``cRDRRTB``. Relative death rate of roots [d⁻¹] as a function of
    DVS."""

    rdrstb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.0), (1.5, 0.0), (1.5001, 0.02), (2.0, 0.02)]
        )
    )
    """``cRDRSTB``. Relative death rate of stems [d⁻¹] as a function of
    DVS."""

    scale_factor_rdr_leaves: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorRDRLeaves``. Scale factor on `rdrltb`."""

    scale_factor_rdr_stems: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorRDRStems``. Scale factor on `rdrstb`."""

    scale_factor_rdr_roots: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorRDRRoots``. Scale factor on `rdrrtb`."""

    # ------------------------------------------------------------------ #
    # 6. Biomass partitioning (from Lintul5.java)
    # ------------------------------------------------------------------ #

    frtb: torch.Tensor = field(
        default_factory=lambda: _table(
            [
                (0.0, 0.50),
                (0.1, 0.50),
                (0.2, 0.40),
                (0.35, 0.22),
                (0.4, 0.17),
                (0.5, 0.13),
                (0.7, 0.07),
                (0.9, 0.03),
                (1.2, 0.0),
                (2.0, 0.0),
            ]
        )
    )
    """``cFRTB``. Fraction of total daily DM growth allocated to **roots**
    as a function of DVS. The remainder (``1 − FR``) is split among leaves,
    stems and storage organs by ``FLTB``, ``FSTB``, ``FOTB``."""

    fltb: torch.Tensor = field(
        default_factory=lambda: _table(
            [
                (0.0, 0.65),
                (0.1, 0.65),
                (0.25, 0.70),
                (0.5, 0.50),
                (0.646, 0.30),
                (0.95, 0.0),
                (2.0, 0.0),
            ]
        )
    )
    """``cFLTB``. Fraction of above-ground DM allocated to **leaves** as a
    function of DVS."""

    fstb: torch.Tensor = field(
        default_factory=lambda: _table(
            [
                (0.0, 0.35),
                (0.1, 0.35),
                (0.25, 0.30),
                (0.5, 0.50),
                (0.646, 0.70),
                (0.95, 1.0),
                (1.0, 0.0),
                (2.0, 0.0),
            ]
        )
    )
    """``cFSTB``. Fraction of above-ground DM allocated to **stems** as a
    function of DVS."""

    fotb: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.0), (0.95, 0.0), (1.0, 1.0), (2.0, 1.0)]
        )
    )
    """``cFOTB``. Fraction of above-ground DM allocated to **storage
    organs** as a function of DVS."""

    # ------------------------------------------------------------------ #
    # 7. NPK demand and concentration limits (from Lintul5.java)
    # ------------------------------------------------------------------ #

    nmxlv: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.06), (0.4, 0.04), (0.7, 0.03), (1.0, 0.02), (2.0, 0.014), (2.1, 0.017)]
        )
    )
    """``cNMXLV``. Maximum N concentration in leaves [g N · g⁻¹ DM] as a
    function of DVS. Drives N demand and the N nutrition index NNI."""

    pmxlv: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.011), (0.4, 0.008), (0.7, 0.006), (1.0, 0.004), (2.0, 0.0027), (2.1, 0.0027)]
        )
    )
    """``cPMXLV``. Maximum P concentration in leaves [g P · g⁻¹ DM] vs
    DVS."""

    kmxlv: torch.Tensor = field(
        default_factory=lambda: _table(
            [(0.0, 0.12), (0.4, 0.08), (0.7, 0.06), (1.0, 0.04), (2.0, 0.028), (2.1, 0.028)]
        )
    )
    """``cKMXLV``. Maximum K concentration in leaves [g K · g⁻¹ DM] vs
    DVS."""

    nmaxso: torch.Tensor = field(default_factory=lambda: _t(0.0176))
    """``cNMAXSO``. Maximum N concentration [g N · g⁻¹ DM] in storage
    organs."""

    pmaxso: torch.Tensor = field(default_factory=lambda: _t(0.0026))
    """``cPMAXSO``. Maximum P concentration in storage organs."""

    kmaxso: torch.Tensor = field(default_factory=lambda: _t(0.0048))
    """``cKMAXSO``. Maximum K concentration in storage organs."""

    lrnr: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cLRNR``. Maximum N concentration in **roots** expressed as a
    fraction of the maximum N concentration in leaves [-]."""

    lrpr: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cLRPR``. As `lrnr` but for P."""

    lrkr: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cLRKR``. As `lrnr` but for K."""

    lsnr: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cLSNR``. Maximum N concentration in **stems** as a fraction of
    the maximum N concentration in leaves [-]."""

    lspr: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cLSPR``. As `lsnr` but for P."""

    lskr: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cLSKR``. As `lsnr` but for K."""

    frnx: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cFRNX``. Optimal N concentration as a fraction of the maximum N
    concentration [-] — controls the N stress index NNI."""

    frpx: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cFRPX``. Optimal P concentration as a fraction of maximum P [-]."""

    frkx: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cFRKX``. Optimal K concentration as a fraction of maximum K [-]."""

    rnflv: torch.Tensor = field(default_factory=lambda: _t(0.004))
    """``cRNFLV``. Residual (non-translocatable) N concentration in
    leaves [g N · g⁻¹ DM]."""

    rnfst: torch.Tensor = field(default_factory=lambda: _t(0.002))
    """``cRNFST``. Residual N concentration in stems."""

    rnfrt: torch.Tensor = field(default_factory=lambda: _t(0.002))
    """``cRNFRT``. Residual N concentration in roots."""

    rpflv: torch.Tensor = field(default_factory=lambda: _t(0.0005))
    """``cRPFLV``. Residual P concentration in leaves."""

    rpfst: torch.Tensor = field(default_factory=lambda: _t(0.0003))
    """``cRPFST``. Residual P concentration in stems."""

    rpfrt: torch.Tensor = field(default_factory=lambda: _t(0.0003))
    """``cRPFRT``. Residual P concentration in roots."""

    rkflv: torch.Tensor = field(default_factory=lambda: _t(0.009))
    """``cRKFLV``. Residual K concentration in leaves."""

    rkfst: torch.Tensor = field(default_factory=lambda: _t(0.005))
    """``cRKFST``. Residual K concentration in stems."""

    rkfrt: torch.Tensor = field(default_factory=lambda: _t(0.005))
    """``cRKFRT``. Residual K concentration in roots."""

    fntrt: torch.Tensor = field(default_factory=lambda: _t(0.15))
    """``cFNTRT``. NPK translocation from **roots** expressed as a
    fraction of the total NPK translocated from leaves and stems [-]."""

    tcnt: torch.Tensor = field(default_factory=lambda: _t(10.0))
    """``cTCNT``. Time constant [d] for N translocation to storage
    organs (first-order kinetics)."""

    tcpt: torch.Tensor = field(default_factory=lambda: _t(10.0))
    """``cTCPT``. Time constant [d] for P translocation to storage
    organs."""

    tckt: torch.Tensor = field(default_factory=lambda: _t(10.0))
    """``cTCKT``. Time constant [d] for K translocation to storage
    organs."""

    nfixf: torch.Tensor = field(default_factory=lambda: _t(0.0))
    """``cNFIXF``. Fraction of crop N uptake supplied by biological N₂
    fixation [-]; > 0 for legumes."""

    # ------------------------------------------------------------------ #
    # 8. Stress sensitivities (from Lintul5.java)
    # ------------------------------------------------------------------ #

    nlue: torch.Tensor = field(default_factory=lambda: _t(1.1))
    """``cNLUE``. Coefficient of the reduction of RUE due to NPK
    (nutrient) stress."""

    nlai: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cNLAI``. Coefficient of the reduction of LAI growth (juvenile
    phase) due to nutrient stress."""

    nsla: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cNSLA``. Coefficient of the reduction of SLA due to nutrient
    stress."""

    npart: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cNPART``. Coefficient of the N-stress effect on leaf biomass
    reduction (re-allocation away from leaves under N deficiency)."""

    # ------------------------------------------------------------------ #
    # 9. DVS thresholds for NPK dynamics (from Lintul5.java)
    # ------------------------------------------------------------------ #

    dvsnt: torch.Tensor = field(default_factory=lambda: _t(0.8))
    """``cDVSNT``. DVS above which N, P and K **translocation** to
    storage organs occurs."""

    dvsnlt: torch.Tensor = field(default_factory=lambda: _t(1.3))
    """``cDVSNLT``. DVS above which crop **uptake** of N, P and K stops."""

    dvsdr: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cDVSDR``. DVS above which death of roots and stems begins."""

    dvsdlt: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cDVSDLT``. DVS above which leaf death (controlled by mean
    temperature, see `rdrltb`) begins."""

    # ------------------------------------------------------------------ #
    # 10. Fertiliser application & recovery (from Lintul5.java)
    # ------------------------------------------------------------------ #

    nrf: torch.Tensor = field(default_factory=lambda: _t(0.7))
    """``cNRF``. Default recovery fraction [0–1] of applied fertiliser N
    (used when no day-resolved `nrftab` is provided)."""

    prf: torch.Tensor = field(default_factory=lambda: _t(0.2))
    """``cPRF``. Default recovery fraction [0–1] of applied fertiliser P."""

    krf: torch.Tensor = field(default_factory=lambda: _t(0.6))
    """``cKRF``. Default recovery fraction [0–1] of applied fertiliser K."""

    nrftab: torch.Tensor | None = None
    """``cNRFTAB``. Optional day-resolved table of N fertiliser recovery
    fractions [-] (shape ``[N, 2]``: x = day, y = fraction)."""

    prftab: torch.Tensor | None = None
    """``cPRFTAB``. Optional day-resolved table of P recovery fractions."""

    krftab: torch.Tensor | None = None
    """``cKRFTAB``. Optional day-resolved table of K recovery fractions."""

    ferntab: torch.Tensor | None = None
    """``cFERNTAB``. Optional table of N fertiliser applications
    [g N · m⁻² · d⁻¹] at given calendar days. Shape ``[N, 2]``."""

    ferptab: torch.Tensor | None = None
    """``cFERPTAB``. Optional table of P fertiliser applications."""

    ferktab: torch.Tensor | None = None
    """``cFERKTAB``. Optional table of K fertiliser applications."""

    scale_factor_fern: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorFERN``. Scale factor on `ferntab` y-values."""

    scale_factor_ferp: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorFERP``. Scale factor on `ferptab` y-values."""

    scale_factor_ferk: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cScaleFactorFERK``. Scale factor on `ferktab` y-values."""

    # ------------------------------------------------------------------ #
    # 11. Run-control flags (from Lintul5.java)
    # ------------------------------------------------------------------ #

    iopt: torch.Tensor = field(default_factory=lambda: _t(1.0))
    """``cIOPT``. Run mode selector:
    ``1`` → optimal (no stresses), ``2`` → water-limited,
    ``3`` → water + N limited, ``4`` → water + N + P + K limited.
    Stored as float so it can be used in masking; cast to int for branching."""

    # ------------------------------------------------------------------ #
    # 12. Heat stress (optional add-on components)
    # ------------------------------------------------------------------ #
    # Crop-specific constants for the optional SIMPLACE heat-stress
    # components, which sit *outside* the core Lintul5 time loop:
    #   * HeatStressOnLeafSenescence — cTempCritical, cFactorAtTempCritical,
    #     cFactorSlope, cDevStageCritical
    #   * HeatStressOnGrain — cTempCritical, cTempLimit, cBeginDevStage,
    #     cEndDevStage
    # Both SIMPLACE components define a constant named ``cTempCritical``;
    # they are disambiguated here with ``leaf_heat_`` / ``grain_heat_``
    # prefixes (mirroring the `Lintul5Model` submodule names). Keeping
    # them in this dataclass lets a per-crop configuration file populate
    # them alongside the core Lintul5 parameters.
    # ------------------------------------------------------------------ #

    leaf_heat_temp_critical: torch.Tensor = field(default_factory=lambda: _t(34.0))
    """``cTempCritical`` of ``HeatStressOnLeafSenescence``. Critical daily
    maximum temperature [°C] above which heat stress accelerates leaf
    senescence."""

    leaf_heat_factor_at_temp_critical: torch.Tensor = field(
        default_factory=lambda: _t(3.0)
    )
    """``cFactorAtTempCritical``. Leaf-senescence multiplier [-] at exactly
    the critical temperature `leaf_heat_temp_critical`."""

    leaf_heat_factor_slope: torch.Tensor = field(default_factory=lambda: _t(0.5))
    """``cFactorSlope``. Increment of the leaf-senescence multiplier per °C
    above `leaf_heat_temp_critical` [°C⁻¹]."""

    leaf_heat_devstage_critical: torch.Tensor = field(
        default_factory=lambda: _t(1.0)
    )
    """``cDevStageCritical``. Development stage [-] above which heat stress
    on leaf senescence applies."""

    grain_heat_temp_critical: torch.Tensor = field(default_factory=lambda: _t(27.0))
    """``cTempCritical`` of ``HeatStressOnGrain``. Critical day-time
    temperature [°C] at which heat stress on grain yield starts."""

    grain_heat_temp_limit: torch.Tensor = field(default_factory=lambda: _t(40.0))
    """``cTempLimit``. Day-time temperature [°C] at which the daily grain
    heat-stress intensity saturates at 1."""

    grain_heat_begin_devstage: torch.Tensor = field(
        default_factory=lambda: _t(0.8)
    )
    """``cBeginDevStage``. Development stage [-] at which the grain
    thermally sensitive window opens."""

    grain_heat_end_devstage: torch.Tensor = field(default_factory=lambda: _t(1.3))
    """``cEndDevStage``. Development stage [-] at which the grain
    thermally sensitive window closes."""

    # ------------------------------------------------------------------ #
    # Init-only inputs (not stored as fields)
    # ------------------------------------------------------------------ #

    crop_name: InitVar = _UNSET
    """Optional crop name selecting a bundled preset from
    ``parameters/crop_data/`` (case- and whitespace-insensitive). When
    omitted, the ``default`` preset is loaded. Pass ``None`` to skip preset
    loading entirely (keep the field values as constructed). Mutually
    exclusive with `config_file`. Not retained as an attribute."""

    config_file: InitVar = None
    """Optional path to a user-provided YAML file (same schema as the
    bundled presets) to load instead of a built-in crop. Mutually exclusive
    with `crop_name`. Not retained as an attribute."""

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

    def __post_init__(
        self, crop_name: Any, config_file: str | Path | None
    ) -> None:
        """Load a crop preset into the parameter fields.

        Resolution:
            * ``config_file`` given → load that YAML file.
            * ``crop_name`` given (a string) → load the matching bundled
              preset via `_builtin_crop_path`.
            * ``crop_name`` omitted and no ``config_file`` → load the
              built-in `_DEFAULT_CROP` preset.
            * ``crop_name=None`` (explicit) → skip loading; retain the field
              values as passed (used internally by `to`).

        Args:
            crop_name: Crop name, ``None`` (skip), or the `_UNSET` sentinel
                (load default).
            config_file: Path to a custom preset YAML, or ``None``.

        Raises:
            ValueError: If both ``crop_name`` and ``config_file`` are given,
                or if ``crop_name`` matches no bundled preset.
            FileNotFoundError: If ``config_file`` does not exist.
        """
        if config_file is not None:
            if crop_name not in (_UNSET, None):
                raise ValueError(
                    "Pass either crop_name or config_file, not both."
                )
            self._apply_preset(_load_preset_file(Path(config_file)))
            return

        if crop_name is None:
            return  # explicit skip — keep field values as constructed

        name = _DEFAULT_CROP if crop_name is _UNSET else crop_name
        self._apply_preset(_load_preset_file(_builtin_crop_path(name)))

    def _apply_preset(self, preset: dict[str, Any]) -> None:
        """Overwrite fields from a parsed preset dict.

        Two preset layouts are supported and may be freely mixed:

        * **Sectioned** (current converter output): a top-level
          ``sections`` mapping whose values are thematic groups
          (``Development``, ``Water Use``, ``Nutrient (N-P-K) Use``,
          ``Root Growth``, ``Stress Response``, …), each carrying its own
          ``scalars`` and/or ``tables`` sub-mapping.
        * **Flat** (legacy / hand-written): top-level ``scalars`` and
          ``tables`` mappings.

        Both forms key on `CropParameters` field names; only the fields
        present are overwritten. Reading both keeps older presets and
        user config files loading unchanged.

        Args:
            preset: Parsed preset mapping in either layout.
        """
        for scalars, tables in _iter_preset_groups(preset):
            for name, value in scalars.items():
                setattr(self, name, _t(float(value)))
            for name, rows in tables.items():
                setattr(
                    self, name, _table([(float(x), float(y)) for x, y in rows])
                )

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

        Checks the two enumerated run-mode selectors against their
        supported discrete domains (per element when batched):

        * ``idsl`` ∈ {0, 1, 2} — phenology mode (temperature only /
          + day length / + vernalisation).
        * ``iopt`` ∈ {1, 2, 3, 4} — run mode (optimal / water-limited /
          + N-limited / + NPK-limited).
        * ``iairdu`` ∈ {0, 1} — root air-ducts flag (``0`` → non-aquatic,
          ``1`` → aquatic, e.g. rice).

        All three are consumed through hard threshold comparisons
        (``idsl >= 1``/``>= 2``; ``iopt <= 2.5``/``<= 3.5``;
        ``iairdu > 0.5``), so an off-domain value would silently snap to
        the nearest mode.

        Raises:
            ValueError: If ``idsl``, ``iopt`` or ``iairdu`` holds an
                unsupported value.
        """
        _check_discrete("crop_params.idsl", self.idsl, (0.0, 1.0, 2.0))
        _check_discrete("crop_params.iopt", self.iopt, (1.0, 2.0, 3.0, 4.0))
        _check_discrete("crop_params.iairdu", self.iairdu, (0.0, 1.0))

    def to(
        self,
        dtype: torch.dtype | None = None,
        device: torch.device | str | None = None,
    ) -> "CropParameters":
        """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 `CropParameters` with every tensor field moved /
            cast; non-tensor fields (e.g., optional tables set to ``None``)
            are copied through unchanged.
        """
        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
        # crop_name=None skips preset loading so the field values built above
        # are preserved rather than being overwritten by the default preset.
        return CropParameters(crop_name=None, **kwargs)

    @classmethod
    def from_crop_name(
        cls,
        crop_name: str,
        dtype: torch.dtype = torch.float32,
    ) -> "CropParameters":
        """Build a `CropParameters` for a named crop preset.

        Equivalent to ``CropParameters(crop_name=crop_name).to(dtype=dtype)``
        but with explicit dtype control.

        Args:
            crop_name: Crop name (e.g. ``"wheat"``, ``"maize"``), case- and
                whitespace-insensitive. See `available_crops` for the full
                list.
            dtype: Target tensor dtype for all scalar/tabular fields.

        Returns:
            A `CropParameters` populated from the requested preset.

        Raises:
            ValueError: If ``crop_name`` matches no bundled preset.
        """
        return cls(crop_name=crop_name).to(dtype=dtype)

cfet: Tensor dataclass-field

cCFET. Crop-specific empirical correction factor [-] applied to the Penman transpiration rate (canopy-resistance proxy).

config_file: InitVar dataclass-field

Optional path to a user-provided YAML file (same schema as the bundled presets) to load instead of a built-in crop. Mutually exclusive with crop_name. Not retained as an attribute.

cotb: Tensor dataclass-field

cCOTB. CO₂ correction factor [-] applied to RUE as a function of atmospheric CO₂ concentration [ppm].

crop_name: InitVar dataclass-field

Optional crop name selecting a bundled preset from parameters/crop_data/ (case- and whitespace-insensitive). When omitted, the default preset is loaded. Pass None to skip preset loading entirely (keep the field values as constructed). Mutually exclusive with config_file. Not retained as an attribute.

day_temp_factor: Tensor dataclass-field

cDayTempFactor. Weight f in T_day = TMAX − f·(TMAX − TMIN) used to derive a daytime mean temperature from min/max. f = 0.25 → daytime mean (default); f = 0.5 → 24-h mean.

depnr: Tensor dataclass-field

cDEPNR. Crop group number [-] for soil-water depletion (Doorenbos & Kassam). Used to compute the critical soil-moisture content above which transpiration is unrestricted.

dtsmtb: Tensor dataclass-field

cDTSMTB (= cTsumIncrementTableMeanTemp × cTsumIncrementTableRate). Daily increment of temperature sum [°C·d] as a function of mean daily air temperature [°C]. Shape [N, 2] with x = T̄, y = ΔTsum/d.

dvsdlt: Tensor dataclass-field

cDVSDLT. DVS above which leaf death (controlled by mean temperature, see rdrltb) begins.

dvsdr: Tensor dataclass-field

cDVSDR. DVS above which death of roots and stems begins.

dvsi: Tensor dataclass-field

cDVSI. Initial development stage of the crop at the start of the simulation (in the range 02).

dvsnlt: Tensor dataclass-field

cDVSNLT. DVS above which crop uptake of N, P and K stops.

dvsnt: Tensor dataclass-field

cDVSNT. DVS above which N, P and K translocation to storage organs occurs.

ferktab: torch.Tensor | None dataclass-field

cFERKTAB. Optional table of K fertiliser applications.

ferntab: torch.Tensor | None dataclass-field

cFERNTAB. Optional table of N fertiliser applications [g N · m⁻² · d⁻¹] at given calendar days. Shape [N, 2].

ferptab: torch.Tensor | None dataclass-field

cFERPTAB. Optional table of P fertiliser applications.

fltb: Tensor dataclass-field

cFLTB. Fraction of above-ground DM allocated to leaves as a function of DVS.

fntrt: Tensor dataclass-field

cFNTRT. NPK translocation from roots expressed as a fraction of the total NPK translocated from leaves and stems [-].

fotb: Tensor dataclass-field

cFOTB. Fraction of above-ground DM allocated to storage organs as a function of DVS.

frkx: Tensor dataclass-field

cFRKX. Optimal K concentration as a fraction of maximum K [-].

frnx: Tensor dataclass-field

cFRNX. Optimal N concentration as a fraction of the maximum N concentration [-] — controls the N stress index NNI.

frpx: Tensor dataclass-field

cFRPX. Optimal P concentration as a fraction of maximum P [-].

frtb: Tensor dataclass-field

cFRTB. Fraction of total daily DM growth allocated to roots as a function of DVS. The remainder (1 − FR) is split among leaves, stems and storage organs by FLTB, FSTB, FOTB.

fstb: Tensor dataclass-field

cFSTB. Fraction of above-ground DM allocated to stems as a function of DVS.

grain_heat_begin_devstage: Tensor dataclass-field

cBeginDevStage. Development stage [-] at which the grain thermally sensitive window opens.

grain_heat_end_devstage: Tensor dataclass-field

cEndDevStage. Development stage [-] at which the grain thermally sensitive window closes.

grain_heat_temp_critical: Tensor dataclass-field

cTempCritical of HeatStressOnGrain. Critical day-time temperature [°C] at which heat stress on grain yield starts.

grain_heat_temp_limit: Tensor dataclass-field

cTempLimit. Day-time temperature [°C] at which the daily grain heat-stress intensity saturates at 1.

iairdu: Tensor dataclass-field

cIAIRDU. Boolean flag (0/1) indicating whether the crop has air ducts in its roots (=1, e.g. rice → tolerates waterlogging) or not (=0). Stored as float for batch broadcasting.

idsl: Tensor dataclass-field

cIDSL. Phenology mode selector: 0 → temperature only; 1 → temperature + day length; 2 → temperature + day length + vernalisation. Stored as float so it can be used in differentiable masking; cast to int for branching.

iopt: Tensor dataclass-field

cIOPT. Run mode selector: 1 → optimal (no stresses), 2 → water-limited, 3 → water + N limited, 4 → water + N + P + K limited. Stored as float so it can be used in masking; cast to int for branching.

kdiftb: Tensor dataclass-field

cKDIFTB. Extinction coefficient [-] for diffuse PAR as a function of DVS.

kmaxso: Tensor dataclass-field

cKMAXSO. Maximum K concentration in storage organs.

kmxlv: Tensor dataclass-field

cKMXLV. Maximum K concentration in leaves [g K · g⁻¹ DM] vs DVS.

krf: Tensor dataclass-field

cKRF. Default recovery fraction [0–1] of applied fertiliser K.

krftab: torch.Tensor | None dataclass-field

cKRFTAB. Optional day-resolved table of K recovery fractions.

laicr: Tensor dataclass-field

cLAICR. Critical leaf area index [m² m⁻²] above which leaves suffer self-shading mortality.

laii: Tensor dataclass-field

LAII initial leaf area index [m² m⁻²] at emergence (Lintul5 output, persisted here as a parameter for re-init).

leaf_heat_devstage_critical: Tensor dataclass-field

cDevStageCritical. Development stage [-] above which heat stress on leaf senescence applies.

leaf_heat_factor_at_temp_critical: Tensor dataclass-field

cFactorAtTempCritical. Leaf-senescence multiplier [-] at exactly the critical temperature leaf_heat_temp_critical.

leaf_heat_factor_slope: Tensor dataclass-field

cFactorSlope. Increment of the leaf-senescence multiplier per °C above leaf_heat_temp_critical [°C⁻¹].

leaf_heat_temp_critical: Tensor dataclass-field

cTempCritical of HeatStressOnLeafSenescence. Critical daily maximum temperature [°C] above which heat stress accelerates leaf senescence.

lrkr: Tensor dataclass-field

cLRKR. As lrnr but for K.

lrnr: Tensor dataclass-field

cLRNR. Maximum N concentration in roots expressed as a fraction of the maximum N concentration in leaves [-].

lrpr: Tensor dataclass-field

cLRPR. As lrnr but for P.

lskr: Tensor dataclass-field

cLSKR. As lsnr but for K.

lsnr: Tensor dataclass-field

cLSNR. Maximum N concentration in stems as a fraction of the maximum N concentration in leaves [-].

lspr: Tensor dataclass-field

cLSPR. As lsnr but for P.

minimal_vernalisation_factor: Tensor dataclass-field

cMinimalVernalisationFactor. Lower bound [0–1] on the vernalisation factor used to limit pheno-rate suppression.

nfixf: Tensor dataclass-field

cNFIXF. Fraction of crop N uptake supplied by biological N₂ fixation [-]; > 0 for legumes.

nlai: Tensor dataclass-field

cNLAI. Coefficient of the reduction of LAI growth (juvenile phase) due to nutrient stress.

nlue: Tensor dataclass-field

cNLUE. Coefficient of the reduction of RUE due to NPK (nutrient) stress.

nmaxso: Tensor dataclass-field

cNMAXSO. Maximum N concentration [g N · g⁻¹ DM] in storage organs.

nmxlv: Tensor dataclass-field

cNMXLV. Maximum N concentration in leaves [g N · g⁻¹ DM] as a function of DVS. Drives N demand and the N nutrition index NNI.

npart: Tensor dataclass-field

cNPART. Coefficient of the N-stress effect on leaf biomass reduction (re-allocation away from leaves under N deficiency).

nrf: Tensor dataclass-field

cNRF. Default recovery fraction [0–1] of applied fertiliser N (used when no day-resolved nrftab is provided).

nrftab: torch.Tensor | None dataclass-field

cNRFTAB. Optional day-resolved table of N fertiliser recovery fractions [-] (shape [N, 2]: x = day, y = fraction).

nsla: Tensor dataclass-field

cNSLA. Coefficient of the reduction of SLA due to nutrient stress.

phottb: Tensor dataclass-field

cPHOTTB (= cPhotoperiodTableHour × cPhotoperiodTableFactor). Photoperiod reduction factor [-] of development rate (until anthesis) as a function of day length [h].

pmaxso: Tensor dataclass-field

cPMAXSO. Maximum P concentration in storage organs.

pmxlv: Tensor dataclass-field

cPMXLV. Maximum P concentration in leaves [g P · g⁻¹ DM] vs DVS.

prf: Tensor dataclass-field

cPRF. Default recovery fraction [0–1] of applied fertiliser P.

prftab: torch.Tensor | None dataclass-field

cPRFTAB. Optional day-resolved table of P recovery fractions.

rdi: Tensor dataclass-field

cRDI. Initial rooting depth [m] (also used by WaterBalance).

rdmcr: Tensor dataclass-field

cRDMCR. Crop-specific maximum rooting depth [m].

rdrl: Tensor dataclass-field

cRDRL. Maximum relative death rate of leaves [d⁻¹] due to water stress.

rdrltb: Tensor dataclass-field

cRDRLTB. Relative death rate of leaves [d⁻¹] as a function of mean daily temperature [°C] (heat-stress curve).

rdrns: Tensor dataclass-field

cRDRNS. Maximum relative death rate of leaves [d⁻¹] due to NPK (nutrient) stress.

rdrrtb: Tensor dataclass-field

cRDRRTB. Relative death rate of roots [d⁻¹] as a function of DVS.

rdrshm: Tensor dataclass-field

cRDRSHM. Maximum relative death rate of leaves [d⁻¹] caused by shading when LAI > laicr.

rdrstb: Tensor dataclass-field

cRDRSTB. Relative death rate of stems [d⁻¹] as a function of DVS.

rgrl: Tensor dataclass-field

cRGRLAI. Maximum relative increase in LAI [d⁻¹] during the juvenile (exponential) phase.

rkflv: Tensor dataclass-field

cRKFLV. Residual K concentration in leaves.

rkfrt: Tensor dataclass-field

cRKFRT. Residual K concentration in roots.

rkfst: Tensor dataclass-field

cRKFST. Residual K concentration in stems.

rnflv: Tensor dataclass-field

cRNFLV. Residual (non-translocatable) N concentration in leaves [g N · g⁻¹ DM].

rnfrt: Tensor dataclass-field

cRNFRT. Residual N concentration in roots.

rnfst: Tensor dataclass-field

cRNFST. Residual N concentration in stems.

rpflv: Tensor dataclass-field

cRPFLV. Residual P concentration in leaves.

rpfrt: Tensor dataclass-field

cRPFRT. Residual P concentration in roots.

rpfst: Tensor dataclass-field

cRPFST. Residual P concentration in stems.

rri: Tensor dataclass-field

cRRI. Maximum daily increase in rooting depth [m d⁻¹].

ruetb: Tensor dataclass-field

cRUETB (= cRUETableDVS × cRUETableRUE). Radiation use efficiency [g DM · MJ⁻¹ PAR] as a function of DVS (declines after grain filling).

scale_factor_ferk: Tensor dataclass-field

cScaleFactorFERK. Scale factor on ferktab y-values.

scale_factor_fern: Tensor dataclass-field

cScaleFactorFERN. Scale factor on ferntab y-values.

scale_factor_ferp: Tensor dataclass-field

cScaleFactorFERP. Scale factor on ferptab y-values.

scale_factor_kdif: Tensor dataclass-field

cScaleFactorKDIF. Scale factor on kdiftb y-values.

scale_factor_rdr_leaves: Tensor dataclass-field

cScaleFactorRDRLeaves. Scale factor on rdrltb.

scale_factor_rdr_roots: Tensor dataclass-field

cScaleFactorRDRRoots. Scale factor on rdrrtb.

scale_factor_rdr_stems: Tensor dataclass-field

cScaleFactorRDRStems. Scale factor on rdrstb.

scale_factor_rue: Tensor dataclass-field

cScaleFactorRUE. Multiplicative scale factor on the y-values of ruetb for sensitivity analysis / calibration.

scale_factor_sla: Tensor dataclass-field

cScaleFactorSLA. Scale factor on slatb y-values.

slatb: Tensor dataclass-field

cSLATB. Specific leaf area [m² g⁻¹] as a function of DVS.

tbase: Tensor dataclass-field

cTBASE. Lower threshold temperature [°C] for LAI increase.

tbasem: Tensor dataclass-field

cTBASEM. Lower threshold (base) temperature [°C] for accumulation of temperature sum before crop emergence.

tckt: Tensor dataclass-field

cTCKT. Time constant [d] for K translocation to storage organs.

tcnt: Tensor dataclass-field

cTCNT. Time constant [d] for N translocation to storage organs (first-order kinetics).

tcpt: Tensor dataclass-field

cTCPT. Time constant [d] for P translocation to storage organs.

tdwi: Tensor dataclass-field

cTDWI. Initial total crop dry weight [g DM m⁻²] at sowing / emergence; partitioned to organs via the partitioning tables.

teffmx: Tensor dataclass-field

cTEFFMX. Maximum effective temperature [°C] used when accumulating temperature sum before emergence (caps TEFF).

tmnftb: Tensor dataclass-field

cTMNFTB. RUE reduction factor [-] as a function of daily minimum temperature [°C] (cold-night stress).

tmpftb: Tensor dataclass-field

cTMPFTB. RUE reduction factor [-] as a function of mean daytime temperature [°C] (high- and low-temperature stress).

tsum1: Tensor dataclass-field

cTSUM1. Required temperature sum [°C·d] from emergence to flowering / anthesis (vegetative period, DVS 0 → 1).

tsum2: Tensor dataclass-field

cTSUM2. Required temperature sum [°C·d] from anthesis to maturity (generative period, DVS 1 → 2).

tsumem: Tensor dataclass-field

cTSUMEM. Required temperature sum [°C·d] from sowing to emergence.

vbase: Tensor dataclass-field

cVBASE. Vernalisation base [thermal day]: vernal days accumulated below this value contribute nothing to the vernalisation factor.

vernalisation_devstage: Tensor dataclass-field

cVernalisationDevStage. Maximum DVS [-] up to which the vernalisation factor is applied; beyond this stage VERNFAC = 1.

vernrt: Tensor dataclass-field

cVERNRT (= cVernalisationTableMeanTemp × cVernalisationTableRate). Daily vernal-day rate [-] as a function of mean daily temperature [°C]. Used only when idsl ≥ 2.

versat: Tensor dataclass-field

cVERSAT. Vernalisation saturation [thermal day]: number of vernal days above which the vernalisation factor is fully released (=1).

__post_init__(self, crop_name, config_file) special

Load a crop preset into the parameter fields.

Resolution

  • config_file given → load that YAML file.
  • crop_name given (a string) → load the matching bundled preset via _builtin_crop_path.
  • crop_name omitted and no config_file → load the built-in _DEFAULT_CROP preset.
  • crop_name=None (explicit) → skip loading; retain the field values as passed (used internally by to).

Parameters:

Name Type Description Default
crop_name Any

Crop name, None (skip), or the _UNSET sentinel (load default).

required
config_file str | Path | None

Path to a custom preset YAML, or None.

required

Exceptions:

Type Description
ValueError

If both crop_name and config_file are given, or if crop_name matches no bundled preset.

FileNotFoundError

If config_file does not exist.

Source code in torchcrop/parameters/crop_params.py
def __post_init__(
    self, crop_name: Any, config_file: str | Path | None
) -> None:
    """Load a crop preset into the parameter fields.

    Resolution:
        * ``config_file`` given → load that YAML file.
        * ``crop_name`` given (a string) → load the matching bundled
          preset via `_builtin_crop_path`.
        * ``crop_name`` omitted and no ``config_file`` → load the
          built-in `_DEFAULT_CROP` preset.
        * ``crop_name=None`` (explicit) → skip loading; retain the field
          values as passed (used internally by `to`).

    Args:
        crop_name: Crop name, ``None`` (skip), or the `_UNSET` sentinel
            (load default).
        config_file: Path to a custom preset YAML, or ``None``.

    Raises:
        ValueError: If both ``crop_name`` and ``config_file`` are given,
            or if ``crop_name`` matches no bundled preset.
        FileNotFoundError: If ``config_file`` does not exist.
    """
    if config_file is not None:
        if crop_name not in (_UNSET, None):
            raise ValueError(
                "Pass either crop_name or config_file, not both."
            )
        self._apply_preset(_load_preset_file(Path(config_file)))
        return

    if crop_name is None:
        return  # explicit skip — keep field values as constructed

    name = _DEFAULT_CROP if crop_name is _UNSET else crop_name
    self._apply_preset(_load_preset_file(_builtin_crop_path(name)))

from_crop_name(crop_name, dtype=torch.float32) classmethod

Build a CropParameters for a named crop preset.

Equivalent to CropParameters(crop_name=crop_name).to(dtype=dtype) but with explicit dtype control.

Parameters:

Name Type Description Default
crop_name str

Crop name (e.g. "wheat", "maize"), case- and whitespace-insensitive. See available_crops for the full list.

required
dtype torch.dtype

Target tensor dtype for all scalar/tabular fields.

torch.float32

Returns:

Type Description
'CropParameters'

A CropParameters populated from the requested preset.

Exceptions:

Type Description
ValueError

If crop_name matches no bundled preset.

Source code in torchcrop/parameters/crop_params.py
@classmethod
def from_crop_name(
    cls,
    crop_name: str,
    dtype: torch.dtype = torch.float32,
) -> "CropParameters":
    """Build a `CropParameters` for a named crop preset.

    Equivalent to ``CropParameters(crop_name=crop_name).to(dtype=dtype)``
    but with explicit dtype control.

    Args:
        crop_name: Crop name (e.g. ``"wheat"``, ``"maize"``), case- and
            whitespace-insensitive. See `available_crops` for the full
            list.
        dtype: Target tensor dtype for all scalar/tabular fields.

    Returns:
        A `CropParameters` populated from the requested preset.

    Raises:
        ValueError: If ``crop_name`` matches no bundled preset.
    """
    return cls(crop_name=crop_name).to(dtype=dtype)

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
'CropParameters'

A new CropParameters with every tensor field moved / cast; non-tensor fields (e.g., optional tables set to None) are copied through unchanged.

Source code in torchcrop/parameters/crop_params.py
def to(
    self,
    dtype: torch.dtype | None = None,
    device: torch.device | str | None = None,
) -> "CropParameters":
    """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 `CropParameters` with every tensor field moved /
        cast; non-tensor fields (e.g., optional tables set to ``None``)
        are copied through unchanged.
    """
    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
    # crop_name=None skips preset loading so the field values built above
    # are preserved rather than being overwritten by the default preset.
    return CropParameters(crop_name=None, **kwargs)

validate(self)

Validate discrete/categorical crop fields.

Checks the two enumerated run-mode selectors against their supported discrete domains (per element when batched):

  • idsl ∈ {0, 1, 2} — phenology mode (temperature only /
  • day length / + vernalisation).
  • iopt ∈ {1, 2, 3, 4} — run mode (optimal / water-limited /
  • N-limited / + NPK-limited).
  • iairdu ∈ {0, 1} — root air-ducts flag (0 → non-aquatic, 1 → aquatic, e.g. rice).

All three are consumed through hard threshold comparisons (idsl >= 1/>= 2; iopt <= 2.5/<= 3.5; iairdu > 0.5), so an off-domain value would silently snap to the nearest mode.

Exceptions:

Type Description
ValueError

If idsl, iopt or iairdu holds an unsupported value.

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

    Checks the two enumerated run-mode selectors against their
    supported discrete domains (per element when batched):

    * ``idsl`` ∈ {0, 1, 2} — phenology mode (temperature only /
      + day length / + vernalisation).
    * ``iopt`` ∈ {1, 2, 3, 4} — run mode (optimal / water-limited /
      + N-limited / + NPK-limited).
    * ``iairdu`` ∈ {0, 1} — root air-ducts flag (``0`` → non-aquatic,
      ``1`` → aquatic, e.g. rice).

    All three are consumed through hard threshold comparisons
    (``idsl >= 1``/``>= 2``; ``iopt <= 2.5``/``<= 3.5``;
    ``iairdu > 0.5``), so an off-domain value would silently snap to
    the nearest mode.

    Raises:
        ValueError: If ``idsl``, ``iopt`` or ``iairdu`` holds an
            unsupported value.
    """
    _check_discrete("crop_params.idsl", self.idsl, (0.0, 1.0, 2.0))
    _check_discrete("crop_params.iopt", self.iopt, (1.0, 2.0, 3.0, 4.0))
    _check_discrete("crop_params.iairdu", self.iairdu, (0.0, 1.0))

available_crops()

Return the built-in crop names loadable via crop_name.

Each name is the stem of a bundled YAML preset in parameters/crop_data/ (e.g. "wheat", "maize", "soybean") and is also accepted, case- and whitespace-insensitively, by CropParameters(crop_name=...).

Returns:

Type Description
tuple[str, ...]

A sorted tuple of available crop names.

Source code in torchcrop/parameters/crop_params.py
@functools.lru_cache(maxsize=1)
def available_crops() -> tuple[str, ...]:
    """Return the built-in crop names loadable via ``crop_name``.

    Each name is the stem of a bundled YAML preset in
    ``parameters/crop_data/`` (e.g. ``"wheat"``, ``"maize"``, ``"soybean"``)
    and is also accepted, case- and whitespace-insensitively, by
    ``CropParameters(crop_name=...)``.

    Returns:
        A sorted tuple of available crop names.
    """
    return tuple(sorted(p.stem for p in _CROP_DATA_DIR.glob("*.yaml")))

default_wheat_params(dtype=torch.float32)

Return the SIMPLACE Lintul5 wheat-like default parameter set.

Loads the built-in default preset (the wheat-like SIMPLACE default), equivalent to CropParameters(crop_name="default").

Parameters:

Name Type Description Default
dtype torch.dtype

Target tensor dtype for all scalar/tabular fields.

torch.float32

Returns:

Type Description
CropParameters

A fresh CropParameters with the Lintul5 default parameters cast to dtype.

Source code in torchcrop/parameters/crop_params.py
def default_wheat_params(dtype: torch.dtype = torch.float32) -> CropParameters:
    """Return the SIMPLACE Lintul5 wheat-like default parameter set.

    Loads the built-in ``default`` preset (the wheat-like SIMPLACE default),
    equivalent to ``CropParameters(crop_name="default")``.

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

    Returns:
        A fresh `CropParameters` with the Lintul5 default parameters
        cast to ``dtype``.
    """
    return CropParameters().to(dtype=dtype)