"""Thin API facade for Kuehne (2022) Norway pine trajectories."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from pyforestry.base.contracts import FormulaDescriptor, SourceReference
from pyforestry.base.helpers import Age, Stand
from pyforestry.base.simulation import GrowthModel, Requirements, SimulationContext
from pyforestry.norway.growth.kuehne_2022 import (
kuehne_2022_basal_area,
kuehne_2022_stand_volume,
kuehne_2022_stem_density,
)
from pyforestry.norway.siteindex.kuehne_2022 import (
KuehnePineModel,
kuehne_2022_height_trajectory_and_si_scots_pine_norway,
kuehne_2022_height_trajectory_scots_pine_norway,
)
[docs]
@dataclass(frozen=True)
class KuehnePineAdapterConfig:
"""Configuration for the Kuehne pine trajectory adapter."""
dominant_height_m: float
start_total_age_years: float
[docs]
class KuehnePineGrowthModel(GrowthModel):
"""Aggregate-safe adapter that updates dominant height trajectories."""
def __init__(self, config: KuehnePineAdapterConfig) -> None:
"""Store trajectory adapter configuration."""
self.config = config
@property
def component_id(self) -> str:
"""Stable identifier for the Kuehne 2022 pine model."""
return "kuehne_2022_pine"
@property
def source(self) -> SourceReference:
"""Bibliographic provenance."""
return 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",
)
[docs]
def requirements(self) -> Requirements:
"""Declare aggregate-mode compatibility."""
return Requirements(inventory="aggregate")
[docs]
def build_context(
self,
stand: Stand,
*,
config: Optional[KuehnePineAdapterConfig] = None,
**kwargs,
) -> SimulationContext:
"""Build context and seed Kuehne trajectory state."""
cfg = config or self.config
ctx = super().build_context(stand, **kwargs)
ctx.state["t"] = float(cfg.start_total_age_years)
ctx.attrs["kuehne_dominant_height_m"] = float(cfg.dominant_height_m)
# Publish the volume at t0 as well as after each step. The equation is the
# same one update_step uses; a model that reports its volume only after
# being stepped cannot state where the stand started, which is the
# opening balance of every run summary.
ctx.attrs["stand_volume_m3_per_ha"] = float(
kuehne_2022_stand_volume(
float(ctx.metrics["BasalArea"]["TOTAL"]),
float(cfg.dominant_height_m),
Age.TOTAL(float(cfg.start_total_age_years)),
)
)
return ctx
[docs]
def update_step(self, ctx: SimulationContext, dt: float) -> None:
"""Advance the stand: dominant height, then stem density, basal area and volume.
Projects one unthinned period using the Kuehne (2022) component equations.
Site index at base age 40 (SI40, required by the stem-density and volume
equations) is derived from the dominant-height trajectory.
"""
if dt <= 0.0:
raise ValueError("dt must be positive.")
age1 = Age.TOTAL(float(ctx.state.get("t", self.config.start_total_age_years)))
age2 = Age.TOTAL(float(age1) + dt)
h1 = float(ctx.attrs.get("kuehne_dominant_height_m", self.config.dominant_height_m))
height2, h100 = kuehne_2022_height_trajectory_and_si_scots_pine_norway(
dominant_height_m=h1,
age=age1,
age2=age2,
output="both",
)
h2 = float(height2)
si40 = float(kuehne_2022_height_trajectory_scots_pine_norway(h1, age1, Age.TOTAL(40.0)))
stems1 = float(ctx.metrics["Stems"]["TOTAL"])
ba1 = float(ctx.metrics["BasalArea"]["TOTAL"])
stems2 = kuehne_2022_stem_density(stems1, age1, age2, si40)
ba2 = kuehne_2022_basal_area(ba1, age1, age2, h1, h2, stems1, stems2)
volume2 = kuehne_2022_stand_volume(ba2, h2, age2)
ctx.set_aggregate_metrics(ba_total=float(ba2), stems_total=float(stems2))
ctx.attrs["kuehne_dominant_height_m"] = h2
ctx.attrs["kuehne_site_index_h100_m"] = float(h100)
ctx.attrs["kuehne_site_index_si40_m"] = si40
ctx.attrs["stand_volume_m3_per_ha"] = float(volume2)
ctx.state["t"] = float(age2)
__all__ = [
"KuehnePineModel",
"KuehnePineAdapterConfig",
"KuehnePineGrowthModel",
"kuehne_2022_height_trajectory_and_si_scots_pine_norway",
]
DESCRIPTOR = FormulaDescriptor(
component_id="kuehne_2022_model",
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",
),
kind="model",
domain="growth",
composes=("kuehne_2022_siteindex",),
kernel_names=("KuehnePineGrowthModel",),
)