fvs-python 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fvs_python-0.2.3.dist-info/METADATA +254 -0
- fvs_python-0.2.3.dist-info/RECORD +149 -0
- fvs_python-0.2.3.dist-info/WHEEL +5 -0
- fvs_python-0.2.3.dist-info/licenses/LICENSE +21 -0
- fvs_python-0.2.3.dist-info/top_level.txt +1 -0
- pyfvs/__init__.py +107 -0
- pyfvs/bark_ratio.py +323 -0
- pyfvs/cfg/CFG_README.md +73 -0
- pyfvs/cfg/dbh_bounding_table_4_7_1_8.json +103 -0
- pyfvs/cfg/ecounit_coefficients_table_4_7_1_5.json +981 -0
- pyfvs/cfg/ecounit_coefficients_table_4_7_1_6.json +856 -0
- pyfvs/cfg/forest_type_mapping_table_4_7_1_4.json +38 -0
- pyfvs/cfg/fortype_coefficients_table_4_7_1_3.json +1183 -0
- pyfvs/cfg/functional_forms.yaml +111 -0
- pyfvs/cfg/growth_model_parameters.yaml +98 -0
- pyfvs/cfg/plant_values_table_4_7_1_7.json +15 -0
- pyfvs/cfg/site_index_transformation.yaml +113 -0
- pyfvs/cfg/sn_bark_ratio_coefficients.json +115 -0
- pyfvs/cfg/sn_crown_competition_factor.json +109 -0
- pyfvs/cfg/sn_crown_ratio_coefficients.json +956 -0
- pyfvs/cfg/sn_crown_width_coefficients.json +1664 -0
- pyfvs/cfg/sn_diameter_growth_coefficients.json +300 -0
- pyfvs/cfg/sn_height_diameter_coefficients.json +97 -0
- pyfvs/cfg/sn_large_tree_diameter_growth.json +191 -0
- pyfvs/cfg/sn_large_tree_height_growth.json +229 -0
- pyfvs/cfg/sn_large_tree_height_growth_coefficients.json +1187 -0
- pyfvs/cfg/sn_mortality_model.json +176 -0
- pyfvs/cfg/sn_regeneration_model.json +252 -0
- pyfvs/cfg/sn_relative_site_index.json +263 -0
- pyfvs/cfg/sn_small_tree_height_growth.json +879 -0
- pyfvs/cfg/sn_species_codes_table.json +728 -0
- pyfvs/cfg/sn_stand_density_index.json +398 -0
- pyfvs/cfg/species/ab_american_basswood.yaml +251 -0
- pyfvs/cfg/species/ae_american_elm.yaml +240 -0
- pyfvs/cfg/species/ah_american_hornbeam.yaml +250 -0
- pyfvs/cfg/species/ap_american_plum.yaml +251 -0
- pyfvs/cfg/species/as_american_sycamore.yaml +253 -0
- pyfvs/cfg/species/ba_black_ash.yaml +254 -0
- pyfvs/cfg/species/bb_basswood.yaml +254 -0
- pyfvs/cfg/species/bc_black_cherry.yaml +254 -0
- pyfvs/cfg/species/bd_sweet_birch.yaml +252 -0
- pyfvs/cfg/species/be_american_beech.yaml +251 -0
- pyfvs/cfg/species/bg_black_gum.yaml +252 -0
- pyfvs/cfg/species/bj_blue_jay.yaml +254 -0
- pyfvs/cfg/species/bk_sugar_maple.yaml +251 -0
- pyfvs/cfg/species/bn_butternut.yaml +252 -0
- pyfvs/cfg/species/bo_red_maple.yaml +255 -0
- pyfvs/cfg/species/bt_bigtooth_aspen.yaml +254 -0
- pyfvs/cfg/species/bu_buckeye.yaml +252 -0
- pyfvs/cfg/species/by_bald_cypress.yaml +255 -0
- pyfvs/cfg/species/ca_american_chestnut.yaml +254 -0
- pyfvs/cfg/species/cb_cucumber_tree.yaml +254 -0
- pyfvs/cfg/species/ck_virginia_pine.yaml +254 -0
- pyfvs/cfg/species/co_pond_cypress.yaml +251 -0
- pyfvs/cfg/species/ct_catalpa.yaml +251 -0
- pyfvs/cfg/species/cw_chestnut_oak.yaml +253 -0
- pyfvs/cfg/species/dw_dogwood.yaml +250 -0
- pyfvs/cfg/species/el_american_hornbeam.yaml +254 -0
- pyfvs/cfg/species/fm_flowering_dogwood.yaml +251 -0
- pyfvs/cfg/species/fr_fraser_fir.yaml +247 -0
- pyfvs/cfg/species/ga_green_ash.yaml +254 -0
- pyfvs/cfg/species/ha_hawthorn.yaml +252 -0
- pyfvs/cfg/species/hb_hornbeam.yaml +254 -0
- pyfvs/cfg/species/hh_dogwood.yaml +251 -0
- pyfvs/cfg/species/hi_hickory_species.yaml +252 -0
- pyfvs/cfg/species/hl_holly.yaml +254 -0
- pyfvs/cfg/species/hm_eastern_hemlock.yaml +246 -0
- pyfvs/cfg/species/hy_holly.yaml +252 -0
- pyfvs/cfg/species/ju_eastern_juniper.yaml +247 -0
- pyfvs/cfg/species/lb_loblolly_bay.yaml +254 -0
- pyfvs/cfg/species/lk_laurel_oak.yaml +254 -0
- pyfvs/cfg/species/ll_longleaf_pine.yaml +265 -0
- pyfvs/cfg/species/lo_silver_maple.yaml +252 -0
- pyfvs/cfg/species/lp_loblolly_pine.yaml +268 -0
- pyfvs/cfg/species/mb_mountain_birch.yaml +250 -0
- pyfvs/cfg/species/mg_magnolia.yaml +251 -0
- pyfvs/cfg/species/ml_maple_leaf.yaml +254 -0
- pyfvs/cfg/species/ms_maple_species.yaml +247 -0
- pyfvs/cfg/species/mv_magnolia_vine.yaml +254 -0
- pyfvs/cfg/species/oh_other_hardwood.yaml +231 -0
- pyfvs/cfg/species/os_other_softwood.yaml +232 -0
- pyfvs/cfg/species/ot_other_tree.yaml +210 -0
- pyfvs/cfg/species/ov_overcup_oak.yaml +254 -0
- pyfvs/cfg/species/pc_pond_cypress.yaml +254 -0
- pyfvs/cfg/species/pd_pitch_pine.yaml +245 -0
- pyfvs/cfg/species/pi_pine_species.yaml +246 -0
- pyfvs/cfg/species/po_american_beech.yaml +254 -0
- pyfvs/cfg/species/pp_pond_pine.yaml +246 -0
- pyfvs/cfg/species/ps_persimmon.yaml +251 -0
- pyfvs/cfg/species/pu_pond_pine.yaml +249 -0
- pyfvs/cfg/species/qs_flowering_dogwood.yaml +254 -0
- pyfvs/cfg/species/ra_red_ash.yaml +245 -0
- pyfvs/cfg/species/rd_redbud.yaml +251 -0
- pyfvs/cfg/species/rl_red_elm.yaml +240 -0
- pyfvs/cfg/species/rm_red_maple.yaml +256 -0
- pyfvs/cfg/species/ro_eastern_hemlock.yaml +255 -0
- pyfvs/cfg/species/sa_slash_pine.yaml +265 -0
- pyfvs/cfg/species/sb_sweet_birch.yaml +255 -0
- pyfvs/cfg/species/sd_sand_pine.yaml +251 -0
- pyfvs/cfg/species/sk_swamp_oak.yaml +253 -0
- pyfvs/cfg/species/sm_sugar_maple.yaml +252 -0
- pyfvs/cfg/species/sn_loblolly_pine.yaml +254 -0
- pyfvs/cfg/species/so_southern_oak.yaml +253 -0
- pyfvs/cfg/species/sp_shortleaf_pine.yaml +267 -0
- pyfvs/cfg/species/sr_spruce_pine.yaml +246 -0
- pyfvs/cfg/species/ss_basswood.yaml +251 -0
- pyfvs/cfg/species/su_sweetgum.yaml +255 -0
- pyfvs/cfg/species/sv_silver_maple.yaml +255 -0
- pyfvs/cfg/species/sy_sycamore.yaml +254 -0
- pyfvs/cfg/species/tm_tamarack.yaml +246 -0
- pyfvs/cfg/species/to_tulip_oak.yaml +254 -0
- pyfvs/cfg/species/ts_tulip_tree.yaml +253 -0
- pyfvs/cfg/species/vp_virginia_pine.yaml +248 -0
- pyfvs/cfg/species/wa_white_ash.yaml +254 -0
- pyfvs/cfg/species/we_white_elm.yaml +250 -0
- pyfvs/cfg/species/wi_willow.yaml +248 -0
- pyfvs/cfg/species/wk_water_oak.yaml +254 -0
- pyfvs/cfg/species/wn_walnut.yaml +254 -0
- pyfvs/cfg/species/wo_white_oak.yaml +256 -0
- pyfvs/cfg/species/wp_white_pine.yaml +250 -0
- pyfvs/cfg/species/wt_water_tupelo.yaml +254 -0
- pyfvs/cfg/species/yp_yellow_poplar.yaml +261 -0
- pyfvs/cfg/species_config.yaml +106 -0
- pyfvs/clark_profile.py +323 -0
- pyfvs/competition.py +332 -0
- pyfvs/config_loader.py +375 -0
- pyfvs/crown_competition_factor.py +464 -0
- pyfvs/crown_ratio.py +377 -0
- pyfvs/crown_width.py +512 -0
- pyfvs/data_export.py +356 -0
- pyfvs/ecological_unit.py +272 -0
- pyfvs/exceptions.py +86 -0
- pyfvs/fia_integration.py +876 -0
- pyfvs/forest_type.py +253 -0
- pyfvs/growth_plots.py +579 -0
- pyfvs/harvest.py +603 -0
- pyfvs/height_diameter.py +248 -0
- pyfvs/large_tree_height_growth.py +822 -0
- pyfvs/logging_config.py +213 -0
- pyfvs/main.py +99 -0
- pyfvs/mortality.py +431 -0
- pyfvs/parameters.py +121 -0
- pyfvs/simulation_engine.py +386 -0
- pyfvs/stand.py +1004 -0
- pyfvs/stand_metrics.py +436 -0
- pyfvs/stand_output.py +552 -0
- pyfvs/tree.py +756 -0
- pyfvs/validation.py +190 -0
- pyfvs/volume_library.py +761 -0
pyfvs/tree.py
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tree class representing an individual tree.
|
|
3
|
+
Implements both small-tree and large-tree growth models.
|
|
4
|
+
"""
|
|
5
|
+
import math
|
|
6
|
+
import yaml
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.stats import weibull_min
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
from .validation import ParameterValidator
|
|
12
|
+
from .logging_config import get_logger, log_model_transition
|
|
13
|
+
|
|
14
|
+
class Tree:
|
|
15
|
+
def __init__(self, dbh, height, species="LP", age=0, crown_ratio=0.85):
|
|
16
|
+
"""Initialize a tree with basic measurements.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
dbh: Diameter at breast height (inches)
|
|
20
|
+
height: Total height (feet)
|
|
21
|
+
species: Species code (e.g., "LP" for loblolly pine)
|
|
22
|
+
age: Tree age in years
|
|
23
|
+
crown_ratio: Initial crown ratio (proportion of tree height with live crown)
|
|
24
|
+
"""
|
|
25
|
+
# Validate parameters
|
|
26
|
+
validated = ParameterValidator.validate_tree_parameters(
|
|
27
|
+
dbh, height, age, crown_ratio, species
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.dbh = validated['dbh']
|
|
31
|
+
self.height = validated['height']
|
|
32
|
+
self.species = species
|
|
33
|
+
self.age = validated['age']
|
|
34
|
+
self.crown_ratio = validated['crown_ratio']
|
|
35
|
+
|
|
36
|
+
# Initialize optional ecounit and forest_type (set during grow())
|
|
37
|
+
self._ecounit = None
|
|
38
|
+
self._forest_type = None
|
|
39
|
+
|
|
40
|
+
# Set up logging
|
|
41
|
+
self.logger = get_logger(__name__)
|
|
42
|
+
|
|
43
|
+
# Check height-DBH relationship (disabled warning for small seedlings)
|
|
44
|
+
if self.height > 4.5 and not ParameterValidator.check_height_dbh_relationship(self.dbh, self.height):
|
|
45
|
+
self.logger.warning(
|
|
46
|
+
f"Unusual height-DBH relationship: DBH={self.dbh}, Height={self.height}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Load configuration
|
|
50
|
+
self._load_config()
|
|
51
|
+
|
|
52
|
+
def _load_config(self):
|
|
53
|
+
"""Load configuration using the new config loader."""
|
|
54
|
+
from .config_loader import get_config_loader
|
|
55
|
+
|
|
56
|
+
loader = get_config_loader()
|
|
57
|
+
|
|
58
|
+
# Load species-specific parameters
|
|
59
|
+
self.species_params = loader.load_species_config(self.species)
|
|
60
|
+
|
|
61
|
+
# Load functional forms and site index parameters
|
|
62
|
+
self.functional_forms = loader.functional_forms
|
|
63
|
+
self.site_index_params = loader.site_index_params
|
|
64
|
+
|
|
65
|
+
# Load growth model parameters
|
|
66
|
+
try:
|
|
67
|
+
growth_params_file = loader.cfg_dir / 'growth_model_parameters.yaml'
|
|
68
|
+
self.growth_params = loader._load_config_file(growth_params_file)
|
|
69
|
+
except Exception:
|
|
70
|
+
# Fallback to defaults if file not found
|
|
71
|
+
self.growth_params = {
|
|
72
|
+
'growth_transitions': {'small_to_large_tree': {'xmin': 1.0, 'xmax': 3.0}},
|
|
73
|
+
'small_tree_growth': {'default': {
|
|
74
|
+
'c1': 1.1421, 'c2': 1.0042, 'c3': -0.0374, 'c4': 0.7632, 'c5': 0.0358
|
|
75
|
+
}}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def grow(self, site_index: float, competition_factor: float, rank: float = 0.5, relsdi: float = 5.0, ba: float = 100, pbal: float = 50, slope: float = 0.05, aspect: float = 0, time_step: int = 5, ecounit: str = None, forest_type: str = None) -> None:
|
|
79
|
+
"""Grow the tree for the specified number of years.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
site_index: Site index (base age 25) in feet
|
|
83
|
+
competition_factor: Competition factor (0-1)
|
|
84
|
+
rank: Tree's rank in diameter distribution (0-1)
|
|
85
|
+
relsdi: Relative stand density index (0-12)
|
|
86
|
+
ba: Stand basal area (sq ft/acre)
|
|
87
|
+
pbal: Plot basal area in larger trees (sq ft/acre)
|
|
88
|
+
slope: Ground slope (proportion)
|
|
89
|
+
aspect: Aspect in radians
|
|
90
|
+
time_step: Number of years to grow the tree (default: 5)
|
|
91
|
+
ecounit: Ecological unit code (e.g., "232", "M231") - passed from Stand
|
|
92
|
+
forest_type: Forest type group (e.g., "FTYLPN") - passed from Stand
|
|
93
|
+
"""
|
|
94
|
+
# Store ecounit and forest_type for use in growth methods
|
|
95
|
+
self._ecounit = ecounit
|
|
96
|
+
self._forest_type = forest_type
|
|
97
|
+
# Validate growth parameters
|
|
98
|
+
validated = ParameterValidator.validate_growth_parameters(
|
|
99
|
+
site_index, competition_factor, ba, pbal, rank, relsdi,
|
|
100
|
+
slope, aspect, time_step, self.species
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Use validated parameters
|
|
104
|
+
site_index = validated['site_index']
|
|
105
|
+
competition_factor = validated['competition_factor']
|
|
106
|
+
ba = validated['ba']
|
|
107
|
+
pbal = validated['pbal']
|
|
108
|
+
rank = validated['rank']
|
|
109
|
+
relsdi = validated['relsdi']
|
|
110
|
+
slope = validated['slope']
|
|
111
|
+
aspect = validated['aspect']
|
|
112
|
+
time_step = validated['time_step']
|
|
113
|
+
|
|
114
|
+
# Store initial values before any changes
|
|
115
|
+
initial_age = self.age
|
|
116
|
+
initial_dbh = self.dbh
|
|
117
|
+
initial_height = self.height
|
|
118
|
+
|
|
119
|
+
# Get transition parameters from config
|
|
120
|
+
transition_params = self.growth_params['growth_transitions']['small_to_large_tree']
|
|
121
|
+
xmin = transition_params['xmin'] # minimum DBH for transition (inches)
|
|
122
|
+
xmax = transition_params['xmax'] # maximum DBH for transition (inches)
|
|
123
|
+
|
|
124
|
+
# Calculate weight for blending growth models based on initial DBH
|
|
125
|
+
# Use smoothstep function for smoother transition (reduces discontinuities)
|
|
126
|
+
if initial_dbh < xmin:
|
|
127
|
+
weight = 0.0
|
|
128
|
+
model_used = "small_tree"
|
|
129
|
+
elif initial_dbh > xmax:
|
|
130
|
+
weight = 1.0
|
|
131
|
+
model_used = "large_tree"
|
|
132
|
+
else:
|
|
133
|
+
# Smoothstep function: 3t² - 2t³ where t = (dbh - xmin) / (xmax - xmin)
|
|
134
|
+
t = (initial_dbh - xmin) / (xmax - xmin)
|
|
135
|
+
weight = t * t * (3.0 - 2.0 * t)
|
|
136
|
+
model_used = "blended"
|
|
137
|
+
|
|
138
|
+
# Log model transition if crossing threshold
|
|
139
|
+
if initial_dbh < xmin and self.dbh >= xmin:
|
|
140
|
+
log_model_transition(self.logger, f"{self.species}_{id(self)}",
|
|
141
|
+
"small_tree", "blended", self.dbh)
|
|
142
|
+
elif initial_dbh < xmax and self.dbh >= xmax:
|
|
143
|
+
log_model_transition(self.logger, f"{self.species}_{id(self)}",
|
|
144
|
+
"blended", "large_tree", self.dbh)
|
|
145
|
+
|
|
146
|
+
# Temporarily increment age for growth calculations
|
|
147
|
+
self.age = initial_age + time_step
|
|
148
|
+
|
|
149
|
+
# Calculate small tree growth
|
|
150
|
+
self._grow_small_tree(site_index, competition_factor, time_step)
|
|
151
|
+
small_dbh = self.dbh
|
|
152
|
+
small_height = self.height
|
|
153
|
+
|
|
154
|
+
# Reset to initial state for large tree model
|
|
155
|
+
self.dbh = initial_dbh
|
|
156
|
+
self.height = initial_height
|
|
157
|
+
|
|
158
|
+
# Calculate large tree growth
|
|
159
|
+
self._grow_large_tree(site_index, competition_factor, ba, pbal, slope, aspect, time_step)
|
|
160
|
+
large_dbh = self.dbh
|
|
161
|
+
large_height = self.height
|
|
162
|
+
|
|
163
|
+
# Blend results based on initial DBH
|
|
164
|
+
self.dbh = (1 - weight) * small_dbh + weight * large_dbh
|
|
165
|
+
self.height = (1 - weight) * small_height + weight * large_height
|
|
166
|
+
|
|
167
|
+
# Ensure age is properly set after growth
|
|
168
|
+
self.age = initial_age + time_step
|
|
169
|
+
|
|
170
|
+
# Update crown ratio using Weibull model (pass time_step for proper scaling)
|
|
171
|
+
self._update_crown_ratio_weibull(rank, relsdi, competition_factor, time_step)
|
|
172
|
+
|
|
173
|
+
def _grow_small_tree(self, site_index, competition_factor, time_step=5):
|
|
174
|
+
"""Implement small tree height growth model using Chapman-Richards function.
|
|
175
|
+
|
|
176
|
+
The Chapman-Richards model (NC-128 form) predicts cumulative height at
|
|
177
|
+
a given age, not periodic growth. Height growth is calculated as the
|
|
178
|
+
difference between heights at future age and current age:
|
|
179
|
+
|
|
180
|
+
Height(t) = c1 * SI^c2 * (1 - exp(c3 * t))^(c4 * SI^c5)
|
|
181
|
+
HeightGrowth = Height(age + time_step) - Height(age)
|
|
182
|
+
|
|
183
|
+
**Time Step Handling:** Unlike the large tree diameter growth model which
|
|
184
|
+
requires explicit scaling (DDS * time_step/5), the Chapman-Richards
|
|
185
|
+
equation naturally handles any time step because it calculates cumulative
|
|
186
|
+
height at discrete ages. The growth increment is simply the difference
|
|
187
|
+
between the height curves at two points in time.
|
|
188
|
+
|
|
189
|
+
**Ecological Unit Effect:** Unlike the large-tree DDS model which applies
|
|
190
|
+
ecounit as an additive term in ln(DDS), the small-tree model does NOT
|
|
191
|
+
apply an ecounit modifier. This is because:
|
|
192
|
+
1. Site Index already incorporates regional productivity - SI=55 means
|
|
193
|
+
height at base age 25 = 55 feet, regardless of region
|
|
194
|
+
2. The Chapman-Richards curve is calibrated to match SI at base age
|
|
195
|
+
3. The FVS large-tree height growth (POTHTG) also does NOT apply ecounit
|
|
196
|
+
4. Ecounit affects DIAMETER growth (DDS), not height growth
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
site_index: Site index (base age 25) in feet
|
|
200
|
+
competition_factor: Competition factor (0-1)
|
|
201
|
+
time_step: Number of years to grow (any positive integer)
|
|
202
|
+
"""
|
|
203
|
+
# Get parameters from config
|
|
204
|
+
small_tree_params = self.growth_params.get('small_tree_growth', {})
|
|
205
|
+
if self.species in small_tree_params:
|
|
206
|
+
p = small_tree_params[self.species]
|
|
207
|
+
else:
|
|
208
|
+
p = small_tree_params.get('default', {
|
|
209
|
+
'c1': 1.1421,
|
|
210
|
+
'c2': 1.0042,
|
|
211
|
+
'c3': -0.0374,
|
|
212
|
+
'c4': 0.7632,
|
|
213
|
+
'c5': 0.0358
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
# Chapman-Richards predicts cumulative height at age t
|
|
217
|
+
# Height(t) = c1 * SI^c2 * (1 - exp(c3 * t))^(c4 * SI^c5)
|
|
218
|
+
#
|
|
219
|
+
# The NC-128 coefficients may have been calibrated with a different site index
|
|
220
|
+
# base age. To ensure Height(base_age=25) = SI, we compute a scaling factor.
|
|
221
|
+
|
|
222
|
+
# Current age (before growth) - age was already incremented in grow()
|
|
223
|
+
current_age = self.age - time_step
|
|
224
|
+
future_age = self.age # This is current_age + time_step
|
|
225
|
+
|
|
226
|
+
# Site index base age for southern pines
|
|
227
|
+
base_age = 25
|
|
228
|
+
|
|
229
|
+
def _raw_chapman_richards(age):
|
|
230
|
+
"""Calculate unscaled Chapman-Richards height."""
|
|
231
|
+
if age <= 0:
|
|
232
|
+
return 1.0
|
|
233
|
+
return (
|
|
234
|
+
p['c1'] * (site_index ** p['c2']) *
|
|
235
|
+
(1.0 - math.exp(p['c3'] * age)) **
|
|
236
|
+
(p['c4'] * (site_index ** p['c5']))
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Calculate scaling factor to ensure Height(base_age) = SI
|
|
240
|
+
# This corrects for NC-128 coefficients that may use different base ages
|
|
241
|
+
raw_height_at_base = _raw_chapman_richards(base_age)
|
|
242
|
+
if raw_height_at_base > 0:
|
|
243
|
+
scale_factor = site_index / raw_height_at_base
|
|
244
|
+
else:
|
|
245
|
+
scale_factor = 1.0
|
|
246
|
+
|
|
247
|
+
# Calculate scaled heights
|
|
248
|
+
if current_age <= 0:
|
|
249
|
+
current_height = 1.0 # Initial height at planting
|
|
250
|
+
else:
|
|
251
|
+
current_height = _raw_chapman_richards(current_age) * scale_factor
|
|
252
|
+
|
|
253
|
+
future_height = _raw_chapman_richards(future_age) * scale_factor
|
|
254
|
+
|
|
255
|
+
# Height growth is the difference
|
|
256
|
+
height_growth = future_height - current_height
|
|
257
|
+
|
|
258
|
+
# NOTE: No ecounit modifier is applied to height growth. Site Index
|
|
259
|
+
# already incorporates regional productivity through the Chapman-Richards
|
|
260
|
+
# curve. However, the ecounit effect IS applied to DIAMETER growth below.
|
|
261
|
+
|
|
262
|
+
# Apply a modifier for competition (subtle effect for small trees)
|
|
263
|
+
# Small trees are less affected by competition than large trees
|
|
264
|
+
max_reduction = self.growth_params.get('competition_effects', {}).get(
|
|
265
|
+
'small_tree_competition', {}).get('max_reduction', 0.2)
|
|
266
|
+
competition_modifier = 1.0 - (max_reduction * competition_factor)
|
|
267
|
+
actual_growth = height_growth * competition_modifier
|
|
268
|
+
|
|
269
|
+
# Update height with bounds checking
|
|
270
|
+
self.height = max(4.5, self.height + actual_growth)
|
|
271
|
+
|
|
272
|
+
# Get ecological unit effect for DIAMETER growth (not height)
|
|
273
|
+
# This matches how FVS applies ecounit to the DDS equation for large trees
|
|
274
|
+
ecounit_multiplier = 1.0
|
|
275
|
+
if self._ecounit is not None:
|
|
276
|
+
from .ecological_unit import get_ecounit_effect
|
|
277
|
+
ecounit_effect = get_ecounit_effect(self.species, self._ecounit)
|
|
278
|
+
# Convert additive ln(DDS) effect to multiplicative diameter increment effect
|
|
279
|
+
# For M231 with LP: ecounit_effect = 0.790, exp(0.790) ≈ 2.2x growth
|
|
280
|
+
ecounit_multiplier = math.exp(ecounit_effect)
|
|
281
|
+
|
|
282
|
+
# Save original DBH before height-diameter update
|
|
283
|
+
original_dbh = self.dbh
|
|
284
|
+
|
|
285
|
+
# Calculate new DBH from height using height-diameter relationship
|
|
286
|
+
self._update_dbh_from_height()
|
|
287
|
+
|
|
288
|
+
# Apply ecounit effect to the DBH INCREMENT (not to height)
|
|
289
|
+
# This ensures regional productivity affects diameter growth
|
|
290
|
+
if ecounit_multiplier != 1.0 and self.dbh > original_dbh:
|
|
291
|
+
dbh_increment = self.dbh - original_dbh
|
|
292
|
+
adjusted_increment = dbh_increment * ecounit_multiplier
|
|
293
|
+
self.dbh = original_dbh + adjusted_increment
|
|
294
|
+
|
|
295
|
+
def _grow_large_tree(self, site_index, competition_factor, ba, pbal, slope, aspect, time_step=5):
|
|
296
|
+
"""Implement large tree diameter growth model using official FVS-SN equations.
|
|
297
|
+
|
|
298
|
+
Based on USDA Forest Service FVS Southern Variant (SN) from dgf.f:
|
|
299
|
+
ln(DDS) = CONSPP + INTERC + LDBH*ln(D) + DBH2*D^2 + LCRWN*ln(CR)
|
|
300
|
+
+ HREL*RELHT + PLTB*BA + PNTBL*PBAL
|
|
301
|
+
+ [forest_type_terms] + [eco_unit_terms] + [plant_effect]
|
|
302
|
+
|
|
303
|
+
Where CONSPP = ISIO*SI + TANS*SLOPE + FCOS*SLOPE*cos(ASPECT) + FSIN*SLOPE*sin(ASPECT)
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
site_index: Site index (base age 50) in feet
|
|
307
|
+
competition_factor: Competition factor (0-1)
|
|
308
|
+
ba: Stand basal area (sq ft/acre), minimum 25.0
|
|
309
|
+
pbal: Plot basal area in larger trees (sq ft/acre)
|
|
310
|
+
slope: Ground slope as tangent (rise/run)
|
|
311
|
+
aspect: Aspect in radians
|
|
312
|
+
time_step: Number of years to grow (default: 5)
|
|
313
|
+
"""
|
|
314
|
+
# Get diameter growth coefficients from species config
|
|
315
|
+
dg_config = self.species_params.get('diameter_growth', {})
|
|
316
|
+
p = dg_config.get('coefficients', {})
|
|
317
|
+
|
|
318
|
+
# Apply FVS bounds
|
|
319
|
+
# BA minimum is 25.0 in FVS
|
|
320
|
+
ba_bounded = max(25.0, ba)
|
|
321
|
+
|
|
322
|
+
# Crown ratio for FVS must be integer percentage (0-100), minimum 25
|
|
323
|
+
# Our crown_ratio is stored as proportion (0-1), convert to percentage
|
|
324
|
+
cr_pct = max(25.0, self.crown_ratio * 100.0)
|
|
325
|
+
|
|
326
|
+
# Relative height (RELHT) = HT/AVH, capped at 1.5 in FVS
|
|
327
|
+
# AVH is the average height of the 40 largest trees (top height)
|
|
328
|
+
# For individual tree growth without stand context, assume codominant (relht = 1.0)
|
|
329
|
+
# This is appropriate for:
|
|
330
|
+
# - Plantation trees that are uniformly managed
|
|
331
|
+
# - Dominant/codominant trees in natural stands
|
|
332
|
+
# When called from Stand.grow(), the Stand should pass actual top height
|
|
333
|
+
# via the _top_height attribute if competition-based height reduction is needed
|
|
334
|
+
if hasattr(self, '_top_height') and self._top_height is not None and self._top_height > 0:
|
|
335
|
+
# Use actual stand top height (passed from Stand)
|
|
336
|
+
relht = min(1.5, self.height / self._top_height)
|
|
337
|
+
else:
|
|
338
|
+
# Default: assume codominant tree (no height suppression)
|
|
339
|
+
# This is appropriate for plantations and dominant trees
|
|
340
|
+
relht = 1.0
|
|
341
|
+
|
|
342
|
+
# Get forest type effect - use passed forest_type or fall back to species config
|
|
343
|
+
fortype_config = self.species_params.get('fortype', {})
|
|
344
|
+
if self._forest_type is not None:
|
|
345
|
+
# Use passed forest type from Stand
|
|
346
|
+
from .forest_type import get_forest_type_effect
|
|
347
|
+
fortype_effect = get_forest_type_effect(self.species, self._forest_type)
|
|
348
|
+
else:
|
|
349
|
+
# Fall back to species config base forest type
|
|
350
|
+
fortype_effect = fortype_config.get('coefficients', {}).get(
|
|
351
|
+
fortype_config.get('base_fortype', 'FTYLPN'), 0.0
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Get ecological unit effect - use passed ecounit or fall back to species config
|
|
355
|
+
if self._ecounit is not None:
|
|
356
|
+
# Use passed ecounit from Stand
|
|
357
|
+
from .ecological_unit import get_ecounit_effect
|
|
358
|
+
ecounit_effect = get_ecounit_effect(self.species, self._ecounit)
|
|
359
|
+
else:
|
|
360
|
+
# Fall back to species config base ecounit (typically 0.0)
|
|
361
|
+
ecounit_config = self.species_params.get('ecounit', {}).get('table_4_7_1_5', {})
|
|
362
|
+
ecounit_effect = ecounit_config.get('coefficients', {}).get(
|
|
363
|
+
ecounit_config.get('base_ecounit', '232'), 0.0
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Get plant effect from species config
|
|
367
|
+
plant_config = self.species_params.get('plant', {})
|
|
368
|
+
plant_effect = plant_config.get('value', 0.0)
|
|
369
|
+
|
|
370
|
+
# Also check growth_params for managed plantation setting
|
|
371
|
+
planting_effects = self.growth_params.get('large_tree_modifiers', {}).get('planting_effect', {})
|
|
372
|
+
if self.species in planting_effects:
|
|
373
|
+
plant_effect = planting_effects[self.species]
|
|
374
|
+
|
|
375
|
+
# Build the diameter growth equation following official FVS structure
|
|
376
|
+
# Coefficient mapping from config (b1-b11) to FVS variables:
|
|
377
|
+
# b1 = INTERC (intercept)
|
|
378
|
+
# b2 = LDBH (ln(DBH) coefficient)
|
|
379
|
+
# b3 = DBH2 (DBH^2 coefficient)
|
|
380
|
+
# b4 = LCRWN (ln(CR) coefficient)
|
|
381
|
+
# b5 = HREL (relative height coefficient)
|
|
382
|
+
# b6 = ISIO (site index coefficient)
|
|
383
|
+
# b7 = PLTB (basal area coefficient) - NOTE: config may have wrong value
|
|
384
|
+
# b8 = PNTBL (point basal area larger coefficient)
|
|
385
|
+
# b9 = TANS (slope tangent coefficient)
|
|
386
|
+
# b10 = FCOS (slope*cos(aspect) coefficient)
|
|
387
|
+
# b11 = FSIN (slope*sin(aspect) coefficient)
|
|
388
|
+
|
|
389
|
+
# Check for new-style coefficient names first, fall back to b1-b11
|
|
390
|
+
interc = p.get('INTERC', p.get('b1', 0.0))
|
|
391
|
+
ldbh = p.get('LDBH', p.get('b2', 0.0))
|
|
392
|
+
dbh2 = p.get('DBH2', p.get('b3', 0.0))
|
|
393
|
+
lcrwn = p.get('LCRWN', p.get('b4', 0.0))
|
|
394
|
+
hrel = p.get('HREL', p.get('b5', 0.0))
|
|
395
|
+
isio = p.get('ISIO', p.get('b6', 0.0))
|
|
396
|
+
pltb = p.get('PLTB', p.get('b7', 0.0))
|
|
397
|
+
pntbl = p.get('PNTBL', p.get('b8', 0.0))
|
|
398
|
+
tans = p.get('TANS', p.get('b9', 0.0))
|
|
399
|
+
fcos = p.get('FCOS', p.get('b10', 0.0))
|
|
400
|
+
fsin = p.get('FSIN', p.get('b11', 0.0))
|
|
401
|
+
|
|
402
|
+
# Calculate CONSPP (site and topographic terms)
|
|
403
|
+
# CONSPP = ISIO*SI + TANS*SLOPE + FCOS*SLOPE*cos(ASPECT) + FSIN*SLOPE*sin(ASPECT)
|
|
404
|
+
conspp = (
|
|
405
|
+
isio * site_index +
|
|
406
|
+
tans * slope +
|
|
407
|
+
fcos * slope * math.cos(aspect) +
|
|
408
|
+
fsin * slope * math.sin(aspect)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Calculate ln(DDS) - change in squared diameter (inside bark)
|
|
412
|
+
# Main equation: ln(DDS) = CONSPP + INTERC + LDBH*ln(D) + DBH2*D^2
|
|
413
|
+
# + LCRWN*ln(CR) + HREL*RELHT + PLTB*BA + PNTBL*PBAL
|
|
414
|
+
# + [forest_type] + [eco_unit] + [plant_effect]
|
|
415
|
+
ln_dds = (
|
|
416
|
+
conspp +
|
|
417
|
+
interc +
|
|
418
|
+
ldbh * math.log(self.dbh) +
|
|
419
|
+
dbh2 * self.dbh**2 +
|
|
420
|
+
lcrwn * math.log(cr_pct) +
|
|
421
|
+
hrel * relht +
|
|
422
|
+
pltb * ba_bounded +
|
|
423
|
+
pntbl * pbal +
|
|
424
|
+
fortype_effect +
|
|
425
|
+
ecounit_effect +
|
|
426
|
+
plant_effect
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Apply FVS minimum bound for ln(DDS)
|
|
430
|
+
ln_dds = max(-9.21, ln_dds)
|
|
431
|
+
|
|
432
|
+
# Convert ln(DDS) to diameter growth
|
|
433
|
+
# DDS is change in squared diameter (inside bark) over growth period
|
|
434
|
+
# Scale growth based on time_step (model is calibrated for 5-year growth)
|
|
435
|
+
dds = math.exp(ln_dds) * (time_step / 5.0)
|
|
436
|
+
|
|
437
|
+
# FVS applies DDS to inside-bark diameter, then converts back to outside-bark
|
|
438
|
+
# From dgdriv.f: D=DBH(I)*BRATIO(...); DG=(SQRT(DSQ+DDS)-D)
|
|
439
|
+
# We must convert DBH to inside-bark, apply DDS, then convert back
|
|
440
|
+
from .bark_ratio import create_bark_ratio_model
|
|
441
|
+
bark_model = create_bark_ratio_model(self.species)
|
|
442
|
+
|
|
443
|
+
# Get bark ratio (DIB/DOB) for current tree
|
|
444
|
+
bark_ratio = bark_model.calculate_bark_ratio(self.dbh)
|
|
445
|
+
|
|
446
|
+
# Convert to inside-bark diameter
|
|
447
|
+
dib_old = self.dbh * bark_ratio
|
|
448
|
+
dib_old_sq = dib_old * dib_old
|
|
449
|
+
|
|
450
|
+
# Apply DDS to inside-bark diameter
|
|
451
|
+
dib_new = math.sqrt(dib_old_sq + dds)
|
|
452
|
+
|
|
453
|
+
# Convert back to outside-bark (DBH)
|
|
454
|
+
self.dbh = dib_new / bark_ratio
|
|
455
|
+
|
|
456
|
+
# Update height using FVS large-tree height growth model (Section 4.7.2)
|
|
457
|
+
# HTG = POTHTG * (0.25 * HGMDCR + 0.75 * HGMDRH)
|
|
458
|
+
self._update_height_large_tree(
|
|
459
|
+
site_index=site_index,
|
|
460
|
+
ba=ba_bounded,
|
|
461
|
+
pbal=pbal,
|
|
462
|
+
slope=slope,
|
|
463
|
+
aspect=aspect,
|
|
464
|
+
relht=relht,
|
|
465
|
+
time_step=time_step,
|
|
466
|
+
competition_factor=competition_factor
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def _update_crown_ratio_weibull(self, rank, relsdi, competition_factor, time_step=5):
|
|
470
|
+
"""Update crown ratio using Weibull-based model with FVS-style change calculation.
|
|
471
|
+
|
|
472
|
+
FVS calculates crown ratio CHANGE, not absolute CR:
|
|
473
|
+
1. Predict "old" CR from start-of-cycle conditions
|
|
474
|
+
2. Predict "new" CR from end-of-cycle conditions
|
|
475
|
+
3. Change = new_prediction - old_prediction
|
|
476
|
+
4. Apply change to actual CR
|
|
477
|
+
|
|
478
|
+
This prevents rapid crown ratio collapse that occurs when
|
|
479
|
+
replacing CR with predicted values directly.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
rank: Tree's rank in diameter distribution (0-1)
|
|
483
|
+
relsdi: Relative stand density index (0-12)
|
|
484
|
+
competition_factor: Competition factor (0-1)
|
|
485
|
+
time_step: Growth cycle length in years (default 5)
|
|
486
|
+
"""
|
|
487
|
+
from .crown_ratio import create_crown_ratio_model
|
|
488
|
+
|
|
489
|
+
# Create crown ratio model for this species
|
|
490
|
+
cr_model = create_crown_ratio_model(self.species)
|
|
491
|
+
|
|
492
|
+
# Calculate CCF from competition factor (rough approximation)
|
|
493
|
+
ccf = 100.0 + 100.0 * competition_factor
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
# Predict crown ratio for current conditions (end of growth cycle)
|
|
497
|
+
predicted_cr = cr_model.predict_individual_crown_ratio(rank, relsdi, ccf)
|
|
498
|
+
|
|
499
|
+
# FVS-style change calculation:
|
|
500
|
+
# The change is bounded to prevent dramatic swings
|
|
501
|
+
# Maximum change is typically 5% per 5-year cycle (1% per year)
|
|
502
|
+
# Scale by time_step to handle different cycle lengths
|
|
503
|
+
max_change_per_cycle = 0.05 * (time_step / 5.0)
|
|
504
|
+
|
|
505
|
+
# Calculate change from current CR toward predicted CR
|
|
506
|
+
change = predicted_cr - self.crown_ratio
|
|
507
|
+
|
|
508
|
+
# Bound the change
|
|
509
|
+
bounded_change = max(-max_change_per_cycle,
|
|
510
|
+
min(max_change_per_cycle, change))
|
|
511
|
+
|
|
512
|
+
# Apply change to current crown ratio
|
|
513
|
+
new_cr = self.crown_ratio + bounded_change
|
|
514
|
+
|
|
515
|
+
# Apply age-related reduction (small gradual decrease with age)
|
|
516
|
+
# Scale by time_step to maintain consistent rate regardless of cycle length
|
|
517
|
+
cr_params = self.growth_params.get('crown_ratio', {})
|
|
518
|
+
age_reduction_rate = cr_params.get('age_reduction', {}).get('rate', 0.001)
|
|
519
|
+
age_reduction = age_reduction_rate * (time_step / 5.0) # Scale per cycle
|
|
520
|
+
new_cr = new_cr * (1.0 - age_reduction)
|
|
521
|
+
|
|
522
|
+
# Ensure reasonable bounds
|
|
523
|
+
self.crown_ratio = max(0.15, min(0.95, new_cr))
|
|
524
|
+
except Exception:
|
|
525
|
+
# Fallback to simpler update if crown ratio calculation fails
|
|
526
|
+
# Use gradual reduction rather than replacement
|
|
527
|
+
# Scale by time_step (2% per 5-year cycle = 0.4% per year)
|
|
528
|
+
reduction = 0.02 * (time_step / 5.0)
|
|
529
|
+
self.crown_ratio = max(0.15, min(0.95,
|
|
530
|
+
self.crown_ratio * (1.0 - reduction)))
|
|
531
|
+
|
|
532
|
+
def _update_dbh_from_height(self):
|
|
533
|
+
"""Update DBH based on height using height-diameter model."""
|
|
534
|
+
from .height_diameter import create_height_diameter_model
|
|
535
|
+
|
|
536
|
+
# Create height-diameter model for this species
|
|
537
|
+
hd_model = create_height_diameter_model(self.species)
|
|
538
|
+
|
|
539
|
+
# Store original DBH to ensure we don't decrease it
|
|
540
|
+
original_dbh = self.dbh
|
|
541
|
+
|
|
542
|
+
if self.height <= 4.5:
|
|
543
|
+
dbw = hd_model.hd_params['curtis_arney']['dbw']
|
|
544
|
+
self.dbh = max(original_dbh, dbw) # Set to minimum DBH, but never decrease
|
|
545
|
+
else:
|
|
546
|
+
# Solve for DBH given target height
|
|
547
|
+
dbh = hd_model.solve_dbh_from_height(
|
|
548
|
+
target_height=self.height,
|
|
549
|
+
initial_dbh=self.dbh
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Ensure DBH never decreases
|
|
553
|
+
self.dbh = max(original_dbh, dbh)
|
|
554
|
+
|
|
555
|
+
def _update_height_from_dbh(self):
|
|
556
|
+
"""Update height based on DBH using height-diameter model."""
|
|
557
|
+
from .height_diameter import create_height_diameter_model
|
|
558
|
+
|
|
559
|
+
# Create height-diameter model for this species
|
|
560
|
+
hd_model = create_height_diameter_model(self.species)
|
|
561
|
+
|
|
562
|
+
# Use the default model specified in configuration
|
|
563
|
+
self.height = hd_model.predict_height(self.dbh)
|
|
564
|
+
|
|
565
|
+
def _update_height_large_tree(
|
|
566
|
+
self,
|
|
567
|
+
site_index: float,
|
|
568
|
+
ba: float = 25.0,
|
|
569
|
+
pbal: float = 0.0,
|
|
570
|
+
slope: float = 0.0,
|
|
571
|
+
aspect: float = 0.0,
|
|
572
|
+
relht: float = 1.0,
|
|
573
|
+
time_step: int = 5,
|
|
574
|
+
competition_factor: float = 0.0
|
|
575
|
+
):
|
|
576
|
+
"""Update height using FVS large-tree height growth model (Section 4.7.2).
|
|
577
|
+
|
|
578
|
+
Delegates to the large_tree_height_growth module which implements the
|
|
579
|
+
FVS Southern variant equations:
|
|
580
|
+
HTG = POTHTG * (0.25 * HGMDCR + 0.75 * HGMDRH)
|
|
581
|
+
|
|
582
|
+
Where:
|
|
583
|
+
- POTHTG = potential height growth from site index curve
|
|
584
|
+
- HGMDCR = crown ratio modifier (Hoerl's Special Function)
|
|
585
|
+
- HGMDRH = relative height modifier (shade tolerance dependent)
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
site_index: Site index (base age 25) in feet
|
|
589
|
+
ba: Stand basal area (sq ft/acre)
|
|
590
|
+
pbal: Plot basal area in larger trees (sq ft/acre)
|
|
591
|
+
slope: Ground slope as tangent (rise/run)
|
|
592
|
+
aspect: Aspect in radians
|
|
593
|
+
relht: Relative height (tree height / top height)
|
|
594
|
+
time_step: Number of years to grow (default: 5)
|
|
595
|
+
competition_factor: Competition factor (0-1), higher = more competition
|
|
596
|
+
"""
|
|
597
|
+
from .large_tree_height_growth import calculate_large_tree_height_growth
|
|
598
|
+
|
|
599
|
+
# Calculate height growth using the module
|
|
600
|
+
htg = calculate_large_tree_height_growth(
|
|
601
|
+
species_code=self.species,
|
|
602
|
+
dbh=self.dbh,
|
|
603
|
+
crown_ratio=self.crown_ratio,
|
|
604
|
+
relative_height=relht,
|
|
605
|
+
site_index=site_index,
|
|
606
|
+
basal_area=ba,
|
|
607
|
+
pbal=pbal,
|
|
608
|
+
slope=slope,
|
|
609
|
+
aspect=aspect,
|
|
610
|
+
tree_age=self.age,
|
|
611
|
+
tree_height=self.height
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Scale for time step (module returns 5-year growth)
|
|
615
|
+
htg = htg * (time_step / 5.0)
|
|
616
|
+
|
|
617
|
+
# Apply competition modifier (consistent with previous implementation)
|
|
618
|
+
competition_effects = self.growth_params.get('competition_effects') or {}
|
|
619
|
+
large_tree_comp = competition_effects.get('large_tree_competition') or {}
|
|
620
|
+
max_reduction = large_tree_comp.get('max_reduction', 0.15)
|
|
621
|
+
competition_modifier = 1.0 - (max_reduction * competition_factor)
|
|
622
|
+
htg = htg * competition_modifier
|
|
623
|
+
|
|
624
|
+
# Update height with bounds checking
|
|
625
|
+
self.height = max(4.5, self.height + htg)
|
|
626
|
+
|
|
627
|
+
def get_volume(self, volume_type: str = 'total_cubic') -> float:
|
|
628
|
+
"""Calculate tree volume using USFS Volume Estimator Library.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
volume_type: Type of volume to return. Options:
|
|
632
|
+
- 'total_cubic': Total cubic volume (default)
|
|
633
|
+
- 'merchantable_cubic': Merchantable cubic volume
|
|
634
|
+
- 'board_foot': Board foot volume
|
|
635
|
+
- 'green_weight': Green weight in pounds
|
|
636
|
+
- 'dry_weight': Dry weight in pounds
|
|
637
|
+
- 'biomass_main_stem': Main stem biomass
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
Volume in specified units
|
|
641
|
+
"""
|
|
642
|
+
from .volume_library import calculate_tree_volume
|
|
643
|
+
|
|
644
|
+
# Calculate volume using NVEL if available, fallback otherwise
|
|
645
|
+
result = calculate_tree_volume(
|
|
646
|
+
dbh=self.dbh,
|
|
647
|
+
height=self.height,
|
|
648
|
+
species_code=self.species
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Return requested volume type
|
|
652
|
+
volume_mapping = {
|
|
653
|
+
'total_cubic': result.total_cubic_volume,
|
|
654
|
+
'gross_cubic': result.gross_cubic_volume,
|
|
655
|
+
'net_cubic': result.net_cubic_volume,
|
|
656
|
+
'merchantable_cubic': result.merchantable_cubic_volume,
|
|
657
|
+
'board_foot': result.board_foot_volume,
|
|
658
|
+
'cord': result.cord_volume,
|
|
659
|
+
'green_weight': result.green_weight,
|
|
660
|
+
'dry_weight': result.dry_weight,
|
|
661
|
+
'sawlog_cubic': result.sawlog_cubic_volume,
|
|
662
|
+
'sawlog_board_foot': result.sawlog_board_foot,
|
|
663
|
+
'biomass_main_stem': result.biomass_main_stem,
|
|
664
|
+
'biomass_live_branches': result.biomass_live_branches,
|
|
665
|
+
'biomass_foliage': result.biomass_foliage
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return volume_mapping.get(volume_type, result.total_cubic_volume)
|
|
669
|
+
|
|
670
|
+
def get_volume_detailed(self) -> dict:
|
|
671
|
+
"""Get detailed volume breakdown for the tree.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Dictionary with all volume types and biomass estimates
|
|
675
|
+
"""
|
|
676
|
+
from .volume_library import calculate_tree_volume
|
|
677
|
+
|
|
678
|
+
result = calculate_tree_volume(
|
|
679
|
+
dbh=self.dbh,
|
|
680
|
+
height=self.height,
|
|
681
|
+
species_code=self.species
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
return result.to_dict()
|
|
685
|
+
|
|
686
|
+
def to_tree_record(self, tree_id: int = 0, year: int = 0,
|
|
687
|
+
ba_percentile: float = 0.0, pbal: float = 0.0,
|
|
688
|
+
prev_dbh: Optional[float] = None,
|
|
689
|
+
prev_height: Optional[float] = None) -> Dict[str, Any]:
|
|
690
|
+
"""Convert tree to FVS-compatible tree record format.
|
|
691
|
+
|
|
692
|
+
Creates a dictionary matching the FVS_TreeList database table schema
|
|
693
|
+
for compatibility with FVS output processing tools.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
tree_id: Unique tree identifier within stand
|
|
697
|
+
year: Stand age/simulation year
|
|
698
|
+
ba_percentile: Basal area percentile (rank) 0-100
|
|
699
|
+
pbal: Point basal area in larger trees (sq ft/acre)
|
|
700
|
+
prev_dbh: Previous period DBH (for growth calculation)
|
|
701
|
+
prev_height: Previous period height (for growth calculation)
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
Dictionary with FVS_TreeList compatible columns:
|
|
705
|
+
- TreeId: Tree identifier
|
|
706
|
+
- Species: Species code
|
|
707
|
+
- Year: Simulation year
|
|
708
|
+
- TPA: Trees per acre (expansion factor)
|
|
709
|
+
- DBH: Diameter at breast height (inches)
|
|
710
|
+
- DG: Diameter growth since last period (inches)
|
|
711
|
+
- Ht: Total height (feet)
|
|
712
|
+
- HtG: Height growth since last period (feet)
|
|
713
|
+
- PctCr: Crown ratio as percent (0-100)
|
|
714
|
+
- CrWidth: Crown width (feet)
|
|
715
|
+
- Age: Tree age (years)
|
|
716
|
+
- BAPctile: Basal area percentile
|
|
717
|
+
- PtBAL: Point basal area larger
|
|
718
|
+
- TcuFt: Total cubic foot volume
|
|
719
|
+
- McuFt: Merchantable cubic foot volume
|
|
720
|
+
- BdFt: Board foot volume (Doyle)
|
|
721
|
+
"""
|
|
722
|
+
from .crown_width import calculate_open_crown_width
|
|
723
|
+
|
|
724
|
+
# Calculate growth since last period
|
|
725
|
+
dg = self.dbh - prev_dbh if prev_dbh is not None else 0.0
|
|
726
|
+
htg = self.height - prev_height if prev_height is not None else 0.0
|
|
727
|
+
|
|
728
|
+
# Calculate crown width
|
|
729
|
+
try:
|
|
730
|
+
cr_width = calculate_open_crown_width(self.species, self.dbh)
|
|
731
|
+
except Exception:
|
|
732
|
+
cr_width = self.dbh * 1.5 # Fallback estimate
|
|
733
|
+
|
|
734
|
+
# Get volumes
|
|
735
|
+
total_cubic = self.get_volume('total_cubic')
|
|
736
|
+
merch_cubic = self.get_volume('merchantable_cubic')
|
|
737
|
+
board_feet = self.get_volume('board_foot')
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
'TreeId': tree_id,
|
|
741
|
+
'Species': self.species,
|
|
742
|
+
'Year': year,
|
|
743
|
+
'TPA': 1.0, # Each tree object represents 1 tree/acre
|
|
744
|
+
'DBH': round(self.dbh, 2),
|
|
745
|
+
'DG': round(dg, 3),
|
|
746
|
+
'Ht': round(self.height, 1),
|
|
747
|
+
'HtG': round(htg, 2),
|
|
748
|
+
'PctCr': round(self.crown_ratio * 100, 1),
|
|
749
|
+
'CrWidth': round(cr_width, 1),
|
|
750
|
+
'Age': self.age,
|
|
751
|
+
'BAPctile': round(ba_percentile, 1),
|
|
752
|
+
'PtBAL': round(pbal, 2),
|
|
753
|
+
'TcuFt': round(total_cubic, 2),
|
|
754
|
+
'McuFt': round(merch_cubic, 2),
|
|
755
|
+
'BdFt': round(board_feet, 1)
|
|
756
|
+
}
|