"""Bengtsson (1978) low-density stand mortality functions."""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from pyforestry.base.contracts import FormulaDescriptor, SourceReference
from ._common import (
calibration_group,
clamp_probability,
normalize_part_of_sweden,
scale_probability_from_5_years,
species_mortality_fraction_from_tree_probabilities,
)
from .types import MortalityContext
def _adjusted_bald(mean_age_years: float) -> float:
"""Return Bengtsson adjusted age term."""
if mean_age_years < 100.0:
return mean_age_years / 10.0 + 1.0
return min(((mean_age_years - 100.0) / 20.0) * 2.0 + 12.0, 17.0)
[docs]
def calibrate_bengtsson(
*,
context: MortalityContext,
tree_probabilities: Sequence[float],
period_years: float = 5.0,
default_stems_per_tree: float = 1.0,
) -> tuple[list[float], dict[str, float], dict[str, float], dict[str, Any]]:
"""Calibrate tree probabilities to Bengtsson species mortality levels.
Reference:
Bengtsson, G. (1978). Beräkning av den naturliga avgången i
avverkningsberäkningarna för 1973 års skogsutrednings slutbetänkande.
In: Skog för framtid, SOU 1978:7, bilaga 6.
Predicts natural mortality in stands of lower (non-self-thinning)
density; in Elfving's (2010) established-stand mortality model this is
combined as a density-weighted average with Söderberg (1986)
self-thinning mortality (see Elfving 2010, "Growth modelling in the
Heureka system").
Args:
context: Unified mortality context.
tree_probabilities: Base tree probabilities before calibration.
period_years: Prediction period in years.
default_stems_per_tree: Fallback represented stems per tree record.
Returns:
A tuple with calibrated probabilities, target species fractions,
correction factors, and diagnostics.
"""
trees = context.trees
stand_conditions = context.stand
site_conditions = context.site
if len(trees) != len(tree_probabilities):
raise ValueError("context.trees and tree_probabilities must have equal length.")
if not trees:
return [], {}, {}, {}
region = normalize_part_of_sweden(site_conditions.part_of_sweden)
south = region == "south"
mean_age_years = (
stand_conditions.mean_age_excl_overstorey_years
if stand_conditions.mean_age_excl_overstorey_years is not None
else stand_conditions.mean_age_total_years
)
if mean_age_years is None:
raise ValueError("mean_age_excl_overstorey_years or mean_age_total_years is required.")
adjusted_bald = _adjusted_bald(float(mean_age_years))
groups_present = {calibration_group(tree.species) for tree in trees}
target_fractions_5_year: dict[str, float] = {}
for group in groups_present:
if group == "pine":
a2 = 0.38 if south else 0.14
elif group == "spruce":
a2 = 0.36 if south else (-0.000236 + 0.0250275 * adjusted_bald)
elif group == "birch":
a2 = 0.46 if south else 0.78
else:
a2 = 0.46 if south else 0.35
target_fractions_5_year[group] = clamp_probability(a2 / 20.0)
target_fractions = {
group: scale_probability_from_5_years(probability_5, period_years)
for group, probability_5 in target_fractions_5_year.items()
}
current_fractions = species_mortality_fraction_from_tree_probabilities(
trees=trees,
tree_probabilities=tree_probabilities,
default_stems_per_tree=default_stems_per_tree,
use_calibration_groups=True,
)
correction_factors: dict[str, float] = {}
for group in groups_present:
current = current_fractions.get(group, 0.0)
target = target_fractions.get(group, 0.0)
correction_factors[group] = target / current if current > 0.0 else 1.0
calibrated_probabilities = [
clamp_probability(
probability * correction_factors.get(calibration_group(tree.species), 1.0)
)
for tree, probability in zip(trees, tree_probabilities, strict=True)
]
diagnostics = {
"part_of_sweden": region,
"adjusted_bald": adjusted_bald,
"current_fractions": current_fractions,
}
return calibrated_probabilities, target_fractions, correction_factors, diagnostics
__all__ = ["calibrate_bengtsson"]
# ---------------------------------------------------------------------------
# Introspection
# ---------------------------------------------------------------------------
DESCRIPTOR = FormulaDescriptor(
component_id="bengtsson_mortality_calibration",
source=SourceReference(
author="Bengtsson, G.",
year=1978,
title=(
"Beräkning av den naturliga avgången i avverkningsberäkningarna "
"för 1973 års skogsutrednings slutbetänkande"
),
note=(
"In: Skog för framtid, SOU 1978:7, bilaga 6. Low-density stand "
"mortality; combined with Söderberg (1986) self-thinning mortality "
"in Elfving's (2010) established-stand mortality model."
),
),
species_groups={},
units={},
kernel_names=("calibrate_bengtsson",),
)