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
pyforestryinstalled 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}}}