Wikberg 2004 ingrowth demo#

This notebook demonstrates the 4-step Wikberg (2004) ingrowth model for a 5-year period. Ingrowth here means trees recruited into the >= 4 cm dbh class. Outputs are per hectare, and ingrowth_to_plot_trees scales the per-ha results to a plot area.

Notebook Objectives#

  • Demonstrate a full 5-year ingrowth workflow using Wikberg (2004).

  • Provide runnable, copy-safe snippets that work in the docs build environment.

Prerequisites#

  • Python environment with pyforestry installed from this repository.

  • Execute cells in order; random components should use fixed seeds where shown.

Sources#

  • Wikberg (2004) ingrowth implementation in pyforestry.sweden domain packages.wikberg_2004_ingrowth.

[1]:
import random

from pyforestry.base.helpers import CircularPlot, Stand, Tree
from pyforestry.base.helpers.primitives.sitebase import SiteBase
from pyforestry.sweden.ingrowth.wikberg_2004 import (
    IngrowthSpeciesGroup,
    Wikberg2004Ingrowth,
    ingrowth_to_plot_trees,
)
from pyforestry.sweden.site import Sweden, SwedishSite
from pyforestry.sweden.siteindex.sis.generated_site_category_trees import (
    predict_site_categories_county_tree,
)
from pyforestry.sweden.siteindex.sis.hagglund_lundmark_1977 import Hagglund_Lundmark_1977_SIS


class SwedishSiteDemo(SwedishSite):
    """Concrete wrapper for notebooks (implements SiteBase abstract method)."""

    def compute_attributes(self) -> None:
        SwedishSite.__post_init__(self)

    def __post_init__(self) -> None:
        SiteBase.__post_init__(self)

1. Define site inputs#

[2]:
site_index_m = 26.0
mean_age_excl_overstorey_years = 60.0
site_category_species = "Picea abies"
county = Sweden.County.KOPPARBERG_OVRIGA
requested_sis = site_index_m

predicted_site_categories = predict_site_categories_county_tree(
    sis_hagglund_1979=requested_sis,
    species=site_category_species,
    Direktlan=county,
)

site = SwedishSiteDemo(
    latitude=60.5,
    longitude=15.0,
    altitude=150.0,
    field_layer=predicted_site_categories["field_layer"],
    bottom_layer=predicted_site_categories["bottom_layer"],
    soil_texture=predicted_site_categories["soil_texture"],
    soil_moisture=predicted_site_categories["soil_moisture"],
    soil_depth=predicted_site_categories["soil_depth"],
    soil_water=predicted_site_categories["soil_water"],
    ditched=predicted_site_categories["ditched"],
)

achieved_sis = Hagglund_Lundmark_1977_SIS(
    species=site_category_species,
    latitude=site.latitude,
    altitude=site.altitude or 0.0,
    soil_moisture=site.soil_moisture,
    ground_layer=site.bottom_layer or Sweden.BottomLayer.FRESH_MOSS,
    vegetation=site.field_layer,
    soil_texture=site.soil_texture or Sweden.SoilTextureTill.SANDY,
    climate_code=site.climate_zone or Sweden.ClimateZone.K1,
    lateral_water=site.soil_water or Sweden.SoilWater.SELDOM_NEVER,
    soil_depth=site.soil_depth or Sweden.SoilDepth.DEEP,
    incline_percent=site.incline_percent or 0.0,
    aspect=site.aspect or 0.0,
    nfi_adjustments=True,
    dlan=site.county or county,
    ditched=bool(site.ditched),
    peat=False,
    gotland=False,
    coast=(site.distance_to_coast or 9999.0) < 50.0,
    limes_norrlandicus=bool(site.n_of_limes_norrlandicus),
)

sis_closeness = {
    "requested_sis": requested_sis,
    "achieved_sis": float(achieved_sis),
    "sis_abs_error": abs(float(achieved_sis) - requested_sis),
    "sis_rel_error_pct": 100.0 * abs(float(achieved_sis) - requested_sis) / requested_sis,
}

display({"sis_closeness": sis_closeness, "predicted_site_categories": predicted_site_categories})
site
{'sis_closeness': {'requested_sis': 26.0,
  'achieved_sis': 28.52447261350454,
  'sis_abs_error': 2.524472613504539,
  'sis_rel_error_pct': 9.709510051940535},
 'predicted_site_categories': {'field_layer': <SwedenFieldLayer.LOW_HERB_WITHOUT_SHRUBS: Vegetation(code=4, swedish_name='Lågört utan ris', english_name='Low-herb without shrubs', index=3)>,
  'bottom_layer': <SwedenBottomLayer.FRESH_MOSS: BottomLayerType(code=6, english_name='Fresh moss type', swedish_name='Friskmosstyp')>,
  'soil_texture': <SwedenSoilTextureTill.SANDY: SoilTextureCategory(code=3, swedish_name='Sandig morän', english_name='Sandy till', short_name='Coarse sand')>,
  'soil_moisture': <SwedenSoilMoisture.MESIC_MOIST: SoilMoistureData(code=3, swedish_description='frisk-fuktig', english_description='Mesic-moist (subsoil water depth <1 m)')>,
  'soil_depth': <SwedenSoilDepth.DEEP: SoilDepthCat(code=1, swedish_description='Mäktigt >70 cm. Inga synliga hällar', english_description='Deep >70cm. No visible stone outcrops.')>,
  'soil_water': <SwedenSoilWater.LONGER_PERIODS: SoilWaterCat(code=3, swedish_description='längre perioder', english_description='Longer periods')>,
  'ditched': False}}
[2]:
SwedishSiteDemo(latitude=60.5, longitude=15.0, altitude=150.0, field_layer=<SwedenFieldLayer.LOW_HERB_WITHOUT_SHRUBS: Vegetation(code=4, swedish_name='Lågört utan ris', english_name='Low-herb without shrubs', index=3)>, bottom_layer=<SwedenBottomLayer.FRESH_MOSS: BottomLayerType(code=6, english_name='Fresh moss type', swedish_name='Friskmosstyp')>, soil_texture=<SwedenSoilTextureTill.SANDY: SoilTextureCategory(code=3, swedish_name='Sandig morän', english_name='Sandy till', short_name='Coarse sand')>, soil_moisture=<SwedenSoilMoisture.MESIC_MOIST: SoilMoistureData(code=3, swedish_description='frisk-fuktig', english_description='Mesic-moist (subsoil water depth <1 m)')>, soil_depth=<SwedenSoilDepth.DEEP: SoilDepthCat(code=1, swedish_description='Mäktigt >70 cm. Inga synliga hällar', english_description='Deep >70cm. No visible stone outcrops.')>, soil_water=<SwedenSoilWater.LONGER_PERIODS: SoilWaterCat(code=3, swedish_description='längre perioder', english_description='Longer periods')>, aspect=None, incline_percent=None, ditched=False, temperature_sum_odin1983=1215.1999999999998, county=<SwedenCounty.KOPPARBERG_OVRIGA: CountyData(code=12, label='Kopparberg (Dalarna), övriga (W övr)')>, humidity=np.float64(75.0), distance_to_coast=np.float64(254.68051713105228), climate_zone=<SwedenClimateZone.M3: ClimateZoneData(code=3, label='M3', description='Maritime, Mountain range')>, sis_spruce_100=None, sis_pine_100=None, sis_birch_50=None, n_of_limes_norrlandicus=True)

2. Build a stand with trees#

[3]:
plot1 = CircularPlot(
    id=1,
    radius_m=5.0,
    trees=[
        Tree(species="picea abies", diameter_cm=24),
        Tree(species="picea abies", diameter_cm=18),
        Tree(species="pinus sylvestris", diameter_cm=26),
        Tree(species="betula pendula", diameter_cm=14),
        Tree(species="picea abies", diameter_cm=3.0),  # sapling
    ],
)
plot2 = CircularPlot(
    id=2,
    radius_m=5.0,
    trees=[
        Tree(species="picea abies", diameter_cm=20),
        Tree(species="pinus sylvestris", diameter_cm=22),
        Tree(species="betula pubescens", diameter_cm=16),
    ],
)

stand = Stand(site=site, plots=[plot1, plot2])
float(stand.BasalArea), float(stand.QMD)

[3]:
(14.604999999999999, 19.108244294021365)

3. Deterministic ingrowth (per ha)#

[4]:
def summarize(result):
    return {
        group.value: {
            "n_small": result.number_small_trees[group],
            "n_ingrowth": result.number_ingrowth_trees[group],
            "mean_dbh_cm": result.mean_diameter_cm[group],
        }
        for group in result.number_ingrowth_trees
    }


model = Wikberg2004Ingrowth(
    deterministic=True,
    ingrowth_species=[
        IngrowthSpeciesGroup.SPRUCE,
        IngrowthSpeciesGroup.PINE,
        IngrowthSpeciesGroup.BIRCH,
    ],
)
result_det = model.predict(
    stand=stand,
    site=site,
    site_index_m=site_index_m,
    mean_age_excl_overstorey_years=mean_age_excl_overstorey_years,
)

summary_det = summarize(result_det)
summary_det
result_det.ingrown_trees

[4]:
[Tree(species=TreeName(genus=TreeGenus(name='Picea', code='PICEA'), species_name='abies', code='PAB'), age=None, diameter_cm=4.933127736061086, height_m=None, imputed=[], position=None, weight_n=45.42766677089276, is_overstorey=None, mortality=None, uid=t9),
 Tree(species=TreeName(genus=TreeGenus(name='Pinus', code='PINUS'), species_name='sylvestris', code='PSYL'), age=None, diameter_cm=5.022826924205686, height_m=None, imputed=[], position=None, weight_n=1.2673878257884912, is_overstorey=None, mortality=None, uid=t10),
 Tree(species=TreeName(genus=TreeGenus(name='Betula', code='BETULA'), species_name='pendula', code='BPEN'), age=None, diameter_cm=5.292627989964315, height_m=None, imputed=[], position=None, weight_n=18.438749292421164, is_overstorey=None, mortality=None, uid=t11)]

4. Convert to plot-level ingrowth trees#

[5]:
plot_ingrowth_trees = ingrowth_to_plot_trees(result_det, plot_area_ha=plot1.area_ha)
plot_ingrowth_trees

[5]:
[Tree(species=TreeName(genus=TreeGenus(name='Picea', code='PICEA'), species_name='abies', code='PAB'), age=None, diameter_cm=4.933127736061086, height_m=None, imputed=[], position=None, weight_n=0.3567880604929047, is_overstorey=None, mortality=None, uid=t12),
 Tree(species=TreeName(genus=TreeGenus(name='Pinus', code='PINUS'), species_name='sylvestris', code='PSYL'), age=None, diameter_cm=5.022826924205686, height_m=None, imputed=[], position=None, weight_n=0.009954040706865662, is_overstorey=None, mortality=None, uid=t13),
 Tree(species=TreeName(genus=TreeGenus(name='Betula', code='BETULA'), species_name='pendula', code='BPEN'), age=None, diameter_cm=5.292627989964315, height_m=None, imputed=[], position=None, weight_n=0.14481759829613583, is_overstorey=None, mortality=None, uid=t14)]

5. Stochastic example (seeded RNG)#

[6]:
rng = random.Random(7)
model_stoch = Wikberg2004Ingrowth(
    deterministic=False,
    rng=rng,
    ingrowth_species=[
        IngrowthSpeciesGroup.SPRUCE,
        IngrowthSpeciesGroup.PINE,
        IngrowthSpeciesGroup.BIRCH,
    ],
)
result_stoch = model_stoch.predict(
    stand=stand,
    site=site,
    site_index_m=site_index_m,
    mean_age_excl_overstorey_years=mean_age_excl_overstorey_years,
)

summary_stoch = summarize(result_stoch)
{"deterministic": summary_det, "stochastic": summary_stoch}

[6]:
{'deterministic': {'spruce': {'n_small': 68.62879599865492,
   'n_ingrowth': 45.42766677089276,
   'mean_dbh_cm': 4.933127736061086},
  'pine': {'n_small': 13.396983990086271,
   'n_ingrowth': 1.2673878257884912,
   'mean_dbh_cm': 5.022826924205686},
  'birch': {'n_small': 33.013908481871226,
   'n_ingrowth': 18.438749292421164,
   'mean_dbh_cm': 5.292627989964315}},
 'stochastic': {'spruce': {'n_small': 68.62879599865492,
   'n_ingrowth': 68.62879599865492,
   'mean_dbh_cm': 4.933127736061086},
  'pine': {'n_small': 0.0, 'n_ingrowth': 0.0, 'mean_dbh_cm': 0.0},
  'birch': {'n_small': 0.0, 'n_ingrowth': 0.0, 'mean_dbh_cm': 0.0}}}