Source code for pyforestry.norway.siteindex.kuehne_2022

"""Kuehne et al. (2022) dominant-height / site-index trajectory for Scots pine, Norway.

Implements the dominant-height / site-index sub-model (Eq. 5) of Kuehne et al.
(2022); the stem-density, basal-area, volume and thinning components (Eqs. 6-10)
are in ``pyforestry.norway.growth.kuehne_2022``. The data are even-aged Scots
pine stands (mostly naturally regenerated or sown, not "planted").
"""

from __future__ import annotations

import warnings
from typing import Callable, Literal

from pyforestry.base.contracts import FormulaDescriptor, SourceReference
from pyforestry.base.helpers.primitives import Age, AgeMeasurement, SiteIndexValue
from pyforestry.base.helpers.tree_species import PINUS_SYLVESTRIS


def _require_total_age(age: AgeMeasurement, argument_name: str) -> None:
    """Validate total-age input for Kuehne equations."""
    if not isinstance(age, AgeMeasurement) or age.code != Age.TOTAL.value:
        raise TypeError(f"Input '{argument_name}' must be specified as Age.TOTAL.")
    if float(age) <= 0.0:
        raise ValueError(f"Input '{argument_name}' must be positive.")


def _kuehne_height_and_h100(
    age_total: float,
    age2_total: float,
    dominant_height_m: float,
) -> tuple[float, float]:
    """Evaluate Kuehne trajectory and SI H100."""
    if dominant_height_m <= 0.0 or age_total <= 0.0 or age2_total <= 0.0:
        raise ValueError("dominant_height_m, age, and age2 must be positive.")

    b1 = 68.41819
    b2 = -24.04110
    b3 = 1.46991

    denom_x = 1.0 - b2 * dominant_height_m * (age_total**-b3)
    if abs(denom_x) < 1e-9:
        warnings.warn("Near-zero denominator in Kuehne trajectory X calculation.", stacklevel=2)
        return float("nan"), float("nan")

    x_term = (dominant_height_m - b1) / denom_x

    denom_h2 = 1.0 + b2 * x_term * (age2_total**-b3)
    if abs(denom_h2) < 1e-9:
        warnings.warn("Near-zero denominator in Kuehne trajectory age2 calculation.", stacklevel=2)
        height2 = float("nan")
    else:
        height2 = (b1 + x_term) / denom_h2

    denom_h100 = 1.0 + b2 * x_term * (100.0**-b3)
    if abs(denom_h100) < 1e-9:
        warnings.warn("Near-zero denominator in Kuehne SI H100 calculation.", stacklevel=2)
        h100 = float("nan")
    else:
        h100 = (b1 + x_term) / denom_h100
    return height2, h100


[docs] def kuehne_2022_height_trajectory_scots_pine_norway( dominant_height_m: float, age: AgeMeasurement, age2: AgeMeasurement, ) -> SiteIndexValue: """Return Scots pine dominant height at total age `age2`.""" _require_total_age(age, "age") _require_total_age(age2, "age2") height2, _ = _kuehne_height_and_h100(float(age), float(age2), float(dominant_height_m)) return SiteIndexValue( value=height2, reference_age=age2, species={PINUS_SYLVESTRIS}, fn=Kuehne2022.height_trajectory.pinus_sylvestris, )
[docs] def kuehne_2022_site_index_h100_scots_pine_norway( dominant_height_m: float, age: AgeMeasurement, ) -> SiteIndexValue: """Return Scots pine site index H100 from current dominant height and total age.""" _require_total_age(age, "age") _, h100 = _kuehne_height_and_h100(float(age), 100.0, float(dominant_height_m)) return SiteIndexValue( value=h100, reference_age=Age.TOTAL(100.0), species={PINUS_SYLVESTRIS}, fn=kuehne_2022_site_index_h100_scots_pine_norway, )
[docs] def kuehne_2022_height_trajectory_and_si_scots_pine_norway( dominant_height_m: float, age: AgeMeasurement, age2: AgeMeasurement, output: Literal["height", "sih100", "both"] = "height", ) -> SiteIndexValue | tuple[SiteIndexValue, float]: """Return trajectory result, SI H100, or both.""" _require_total_age(age, "age") _require_total_age(age2, "age2") height2, h100 = _kuehne_height_and_h100(float(age), float(age2), float(dominant_height_m)) height_value = SiteIndexValue( value=height2, reference_age=age2, species={PINUS_SYLVESTRIS}, fn=Kuehne2022.height_trajectory.pinus_sylvestris, ) key = output.lower() if key == "height": return height_value if key == "sih100": return SiteIndexValue( value=h100, reference_age=Age.TOTAL(100.0), species={PINUS_SYLVESTRIS}, fn=kuehne_2022_site_index_h100_scots_pine_norway, ) if key == "both": return height_value, h100 raise ValueError("output must be one of: 'height', 'sih100', 'both'.")
class _Wrapper: """Callable wrapper for class-style compatibility access.""" def __init__(self, fn: Callable): """Init. Source: Forestry model implementation for Norwegian conditions as provided by pyforestry equation modules. """ self._fn = fn def __call__(self, *args, **kwargs): """Delegate to wrapped function.""" return self._fn(*args, **kwargs)
[docs] class Kuehne2022: """Class-style compatibility facade for Kuehne (2022).""" height_trajectory = type( "HeightTrajectoryContainer", (), {"pinus_sylvestris": _Wrapper(kuehne_2022_height_trajectory_scots_pine_norway)}, )()
[docs] class KuehnePineModel: """Thin compatibility class exposing Kuehne height/site-index callables.""" height_trajectory_and_si = staticmethod(kuehne_2022_height_trajectory_and_si_scots_pine_norway)
__all__ = [ "Kuehne2022", "KuehnePineModel", "kuehne_2022_height_trajectory_scots_pine_norway", "kuehne_2022_site_index_h100_scots_pine_norway", "kuehne_2022_height_trajectory_and_si_scots_pine_norway", ] DESCRIPTOR = FormulaDescriptor( component_id="kuehne_2022_siteindex", source=SourceReference( author="Kuehne, C., McLean, J.P., Maleki, K., Antón-Fernández, C. & Astrup, R.", year=2022, title=( "A stand-level growth and yield model for thinned and unthinned " "even-aged Scots pine forests in Norway" ), note=( "Silva Fennica 56(1), article 10627. doi:10.14214/sf.10627. " "Dominant-height / site-index sub-model, Eq. 5." ), ), species_groups={"pine": frozenset({"Pinus sylvestris"})}, units={}, kernel_names=( "Kuehne2022", "KuehnePineModel", "kuehne_2022_height_trajectory_scots_pine_norway", "kuehne_2022_site_index_h100_scots_pine_norway", "kuehne_2022_height_trajectory_and_si_scots_pine_norway", ), )