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/stand_metrics.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stand metrics calculator for FVS-Python.
|
|
3
|
+
|
|
4
|
+
This module provides stand-level metric calculations extracted from the Stand class
|
|
5
|
+
to improve testability and maintainability. All calculations follow FVS Southern
|
|
6
|
+
variant specifications.
|
|
7
|
+
|
|
8
|
+
Metrics include:
|
|
9
|
+
- Crown Competition Factor (CCF) using equation 4.5.1
|
|
10
|
+
- Quadratic Mean Diameter (QMD)
|
|
11
|
+
- Basal Area (BA)
|
|
12
|
+
- Stand Density Index (SDI) using Reineke's equation
|
|
13
|
+
- Relative SDI (RELSDI)
|
|
14
|
+
- Top Height
|
|
15
|
+
- Point Basal Area in Larger trees (PBAL)
|
|
16
|
+
"""
|
|
17
|
+
import math
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .tree import Tree
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StandMetricsCalculator:
|
|
27
|
+
"""Calculator for stand-level metrics.
|
|
28
|
+
|
|
29
|
+
Provides all stand metric calculations in a standalone class that can be
|
|
30
|
+
tested independently and reused across different components.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
default_species: Default species code for SDI calculations
|
|
34
|
+
_sdi_maximums: Cached SDI maximum values by species
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Class-level cache for SDI maximums (shared across instances)
|
|
38
|
+
_sdi_maximums: Optional[Dict[str, int]] = None
|
|
39
|
+
_sdi_loaded: bool = False
|
|
40
|
+
|
|
41
|
+
def __init__(self, default_species: str = 'LP'):
|
|
42
|
+
"""Initialize the metrics calculator.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
default_species: Default species code for SDI lookups
|
|
46
|
+
"""
|
|
47
|
+
self.default_species = default_species
|
|
48
|
+
|
|
49
|
+
# Load SDI maximums if not already loaded
|
|
50
|
+
if not StandMetricsCalculator._sdi_loaded:
|
|
51
|
+
self._load_sdi_maximums()
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def _load_sdi_maximums(cls) -> None:
|
|
55
|
+
"""Load SDI maximum values from configuration."""
|
|
56
|
+
try:
|
|
57
|
+
sdi_file = Path(__file__).parent.parent.parent / "cfg" / "sn_stand_density_index.json"
|
|
58
|
+
if sdi_file.exists():
|
|
59
|
+
with open(sdi_file, 'r') as f:
|
|
60
|
+
sdi_data = json.load(f)
|
|
61
|
+
cls._sdi_maximums = {
|
|
62
|
+
species: data['sdi_maximum']
|
|
63
|
+
for species, data in sdi_data.get('sdi_maximums', {}).items()
|
|
64
|
+
}
|
|
65
|
+
else:
|
|
66
|
+
cls._sdi_maximums = {'LP': 480, 'SP': 490, 'SA': 385, 'LL': 332}
|
|
67
|
+
cls._sdi_loaded = True
|
|
68
|
+
except Exception:
|
|
69
|
+
cls._sdi_maximums = {'LP': 480, 'SP': 490, 'SA': 385, 'LL': 332}
|
|
70
|
+
cls._sdi_loaded = True
|
|
71
|
+
|
|
72
|
+
def calculate_all_metrics(self, trees: List['Tree'], species: str = None) -> Dict[str, float]:
|
|
73
|
+
"""Calculate all stand metrics in a single pass for efficiency.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
trees: List of Tree objects in the stand
|
|
77
|
+
species: Species code for SDI calculations (uses default if None)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary containing all calculated metrics:
|
|
81
|
+
- tpa: Trees per acre
|
|
82
|
+
- ba: Basal area (sq ft/acre)
|
|
83
|
+
- qmd: Quadratic mean diameter (inches)
|
|
84
|
+
- top_height: Average height of 40 largest trees (feet)
|
|
85
|
+
- ccf: Crown Competition Factor
|
|
86
|
+
- sdi: Stand Density Index
|
|
87
|
+
- max_sdi: Maximum SDI for species composition
|
|
88
|
+
- relsdi: Relative SDI (1.0-12.0)
|
|
89
|
+
"""
|
|
90
|
+
species = species or self.default_species
|
|
91
|
+
|
|
92
|
+
if not trees:
|
|
93
|
+
return {
|
|
94
|
+
'tpa': 0,
|
|
95
|
+
'ba': 0.0,
|
|
96
|
+
'qmd': 0.0,
|
|
97
|
+
'top_height': 0.0,
|
|
98
|
+
'ccf': 0.0,
|
|
99
|
+
'sdi': 0.0,
|
|
100
|
+
'max_sdi': self._sdi_maximums.get(species, 480),
|
|
101
|
+
'relsdi': 1.0
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Calculate basic metrics in single pass
|
|
105
|
+
n_trees = len(trees)
|
|
106
|
+
sum_dbh_squared = 0.0
|
|
107
|
+
total_ba = 0.0
|
|
108
|
+
species_ba: Dict[str, float] = {}
|
|
109
|
+
|
|
110
|
+
for tree in trees:
|
|
111
|
+
dbh = tree.dbh
|
|
112
|
+
tree_species = getattr(tree, 'species', species)
|
|
113
|
+
|
|
114
|
+
sum_dbh_squared += dbh ** 2
|
|
115
|
+
ba = math.pi * (dbh / 24.0) ** 2
|
|
116
|
+
total_ba += ba
|
|
117
|
+
species_ba[tree_species] = species_ba.get(tree_species, 0.0) + ba
|
|
118
|
+
|
|
119
|
+
# Derived metrics
|
|
120
|
+
qmd = math.sqrt(sum_dbh_squared / n_trees) if n_trees > 0 else 0.0
|
|
121
|
+
sdi = n_trees * ((qmd / 10.0) ** 1.605) if qmd > 0 else 0.0
|
|
122
|
+
|
|
123
|
+
# Max SDI (basal area weighted)
|
|
124
|
+
max_sdi = self._calculate_weighted_max_sdi(species_ba, total_ba, species)
|
|
125
|
+
|
|
126
|
+
# Relative SDI
|
|
127
|
+
relsdi = (sdi / max_sdi) * 10.0 if max_sdi > 0 else 1.0
|
|
128
|
+
relsdi = max(1.0, min(12.0, relsdi))
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
'tpa': n_trees,
|
|
132
|
+
'ba': total_ba,
|
|
133
|
+
'qmd': qmd,
|
|
134
|
+
'top_height': self.calculate_top_height(trees),
|
|
135
|
+
'ccf': self.calculate_ccf(trees),
|
|
136
|
+
'sdi': sdi,
|
|
137
|
+
'max_sdi': max_sdi,
|
|
138
|
+
'relsdi': relsdi
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def _calculate_weighted_max_sdi(
|
|
142
|
+
self,
|
|
143
|
+
species_ba: Dict[str, float],
|
|
144
|
+
total_ba: float,
|
|
145
|
+
default_species: str
|
|
146
|
+
) -> float:
|
|
147
|
+
"""Calculate basal area-weighted maximum SDI.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
species_ba: Dictionary of basal area by species
|
|
151
|
+
total_ba: Total stand basal area
|
|
152
|
+
default_species: Default species if no trees
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Weighted maximum SDI
|
|
156
|
+
"""
|
|
157
|
+
if total_ba == 0:
|
|
158
|
+
return self._sdi_maximums.get(default_species, 480)
|
|
159
|
+
|
|
160
|
+
weighted_sdi = 0.0
|
|
161
|
+
for species, ba in species_ba.items():
|
|
162
|
+
species_sdi = self._sdi_maximums.get(species, 400)
|
|
163
|
+
weighted_sdi += (ba / total_ba) * species_sdi
|
|
164
|
+
|
|
165
|
+
return weighted_sdi
|
|
166
|
+
|
|
167
|
+
def calculate_ccf(self, trees: List['Tree']) -> float:
|
|
168
|
+
"""Calculate Crown Competition Factor using official FVS equation 4.5.1.
|
|
169
|
+
|
|
170
|
+
Uses open-grown crown widths for each tree:
|
|
171
|
+
- CCFt = 0.001803 * OCW² (for DBH > 0.1 inches)
|
|
172
|
+
- CCFt = 0.001 (for DBH ≤ 0.1 inches)
|
|
173
|
+
- Stand CCF = Σ CCFt
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
trees: List of Tree objects
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Stand-level Crown Competition Factor
|
|
180
|
+
"""
|
|
181
|
+
if not trees:
|
|
182
|
+
return 0.0
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
from .crown_width import CrownWidthModel
|
|
186
|
+
use_crown_model = True
|
|
187
|
+
except ImportError:
|
|
188
|
+
use_crown_model = False
|
|
189
|
+
|
|
190
|
+
CCF_COEFFICIENT = 0.001803
|
|
191
|
+
SMALL_TREE_CCF = 0.001
|
|
192
|
+
DBH_THRESHOLD = 0.1
|
|
193
|
+
|
|
194
|
+
total_ccf = 0.0
|
|
195
|
+
|
|
196
|
+
for tree in trees:
|
|
197
|
+
dbh = getattr(tree, 'dbh', 0.0)
|
|
198
|
+
species = getattr(tree, 'species', self.default_species)
|
|
199
|
+
|
|
200
|
+
if dbh <= DBH_THRESHOLD:
|
|
201
|
+
total_ccf += SMALL_TREE_CCF
|
|
202
|
+
elif use_crown_model:
|
|
203
|
+
try:
|
|
204
|
+
model = CrownWidthModel(species)
|
|
205
|
+
ocw = model.calculate_open_grown_crown_width(dbh)
|
|
206
|
+
tree_ccf = CCF_COEFFICIENT * (ocw ** 2)
|
|
207
|
+
total_ccf += tree_ccf
|
|
208
|
+
except Exception:
|
|
209
|
+
# Fallback: estimate OCW linearly
|
|
210
|
+
ocw_estimate = 3.0 + 0.15 * dbh
|
|
211
|
+
total_ccf += CCF_COEFFICIENT * (ocw_estimate ** 2)
|
|
212
|
+
else:
|
|
213
|
+
# Fallback: estimate OCW linearly
|
|
214
|
+
ocw_estimate = 3.0 + 0.15 * dbh
|
|
215
|
+
total_ccf += CCF_COEFFICIENT * (ocw_estimate ** 2)
|
|
216
|
+
|
|
217
|
+
return total_ccf
|
|
218
|
+
|
|
219
|
+
def calculate_qmd(self, trees: List['Tree']) -> float:
|
|
220
|
+
"""Calculate Quadratic Mean Diameter (QMD).
|
|
221
|
+
|
|
222
|
+
QMD = sqrt(sum(DBH²) / n)
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
trees: List of Tree objects
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Quadratic mean diameter in inches
|
|
229
|
+
"""
|
|
230
|
+
if not trees:
|
|
231
|
+
return 0.0
|
|
232
|
+
|
|
233
|
+
sum_dbh_squared = sum(tree.dbh ** 2 for tree in trees)
|
|
234
|
+
n = len(trees)
|
|
235
|
+
|
|
236
|
+
return math.sqrt(sum_dbh_squared / n)
|
|
237
|
+
|
|
238
|
+
def calculate_top_height(self, trees: List['Tree'], n_trees: int = 40) -> float:
|
|
239
|
+
"""Calculate top height (average height of largest trees by DBH).
|
|
240
|
+
|
|
241
|
+
Top height is defined in FVS as the average height of the 40 largest
|
|
242
|
+
(by DBH) trees per acre. This is used in site index calculations and
|
|
243
|
+
as a measure of dominant stand height.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
trees: List of Tree objects
|
|
247
|
+
n_trees: Number of largest trees to include (default 40 per FVS standard)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Top height in feet (average height of n largest trees by DBH)
|
|
251
|
+
"""
|
|
252
|
+
if not trees:
|
|
253
|
+
return 0.0
|
|
254
|
+
|
|
255
|
+
# Sort trees by DBH descending and take the largest n
|
|
256
|
+
sorted_trees = sorted(trees, key=lambda t: t.dbh, reverse=True)
|
|
257
|
+
top_trees = sorted_trees[:min(n_trees, len(sorted_trees))]
|
|
258
|
+
|
|
259
|
+
if not top_trees:
|
|
260
|
+
return 0.0
|
|
261
|
+
|
|
262
|
+
return sum(tree.height for tree in top_trees) / len(top_trees)
|
|
263
|
+
|
|
264
|
+
def calculate_basal_area(self, trees: List['Tree']) -> float:
|
|
265
|
+
"""Calculate stand basal area.
|
|
266
|
+
|
|
267
|
+
BA = Σ (π * (DBH/24)²) [sq ft per acre]
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
trees: List of Tree objects
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Total basal area in square feet per acre
|
|
274
|
+
"""
|
|
275
|
+
if not trees:
|
|
276
|
+
return 0.0
|
|
277
|
+
|
|
278
|
+
return sum(math.pi * (tree.dbh / 24.0) ** 2 for tree in trees)
|
|
279
|
+
|
|
280
|
+
def calculate_sdi(self, trees: List['Tree']) -> float:
|
|
281
|
+
"""Calculate Stand Density Index using Reineke's equation.
|
|
282
|
+
|
|
283
|
+
SDI = TPA * (QMD / 10)^1.605
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
trees: List of Tree objects
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Stand Density Index
|
|
290
|
+
"""
|
|
291
|
+
if not trees:
|
|
292
|
+
return 0.0
|
|
293
|
+
|
|
294
|
+
tpa = len(trees)
|
|
295
|
+
qmd = self.calculate_qmd(trees)
|
|
296
|
+
|
|
297
|
+
if qmd <= 0:
|
|
298
|
+
return 0.0
|
|
299
|
+
|
|
300
|
+
return tpa * ((qmd / 10.0) ** 1.605)
|
|
301
|
+
|
|
302
|
+
def calculate_relsdi(self, trees: List['Tree'], species: str = None) -> float:
|
|
303
|
+
"""Calculate Relative Stand Density Index (RELSDI).
|
|
304
|
+
|
|
305
|
+
RELSDI = (Stand_SDI / Max_SDI) * 10
|
|
306
|
+
Bounded between 1.0 and 12.0 per FVS specification.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
trees: List of Tree objects
|
|
310
|
+
species: Species code for SDI max lookup
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Relative SDI value (1.0-12.0)
|
|
314
|
+
"""
|
|
315
|
+
species = species or self.default_species
|
|
316
|
+
|
|
317
|
+
stand_sdi = self.calculate_sdi(trees)
|
|
318
|
+
max_sdi = self.get_max_sdi(trees, species)
|
|
319
|
+
|
|
320
|
+
if max_sdi <= 0:
|
|
321
|
+
return 1.0
|
|
322
|
+
|
|
323
|
+
relsdi = (stand_sdi / max_sdi) * 10.0
|
|
324
|
+
|
|
325
|
+
# Apply FVS bounds
|
|
326
|
+
return max(1.0, min(12.0, relsdi))
|
|
327
|
+
|
|
328
|
+
def get_max_sdi(self, trees: List['Tree'], default_species: str = None) -> float:
|
|
329
|
+
"""Get maximum SDI for the stand based on species composition.
|
|
330
|
+
|
|
331
|
+
Uses basal area-weighted average of species-specific SDI maximums.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
trees: List of Tree objects
|
|
335
|
+
default_species: Species to use if no trees
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Maximum SDI for the stand
|
|
339
|
+
"""
|
|
340
|
+
default_species = default_species or self.default_species
|
|
341
|
+
|
|
342
|
+
if not trees:
|
|
343
|
+
return self._sdi_maximums.get(default_species, 480)
|
|
344
|
+
|
|
345
|
+
# Calculate basal area by species
|
|
346
|
+
species_ba: Dict[str, float] = {}
|
|
347
|
+
total_ba = 0.0
|
|
348
|
+
|
|
349
|
+
for tree in trees:
|
|
350
|
+
species = getattr(tree, 'species', default_species)
|
|
351
|
+
ba = math.pi * (tree.dbh / 24.0) ** 2
|
|
352
|
+
species_ba[species] = species_ba.get(species, 0.0) + ba
|
|
353
|
+
total_ba += ba
|
|
354
|
+
|
|
355
|
+
return self._calculate_weighted_max_sdi(species_ba, total_ba, default_species)
|
|
356
|
+
|
|
357
|
+
def calculate_pbal(self, trees: List['Tree'], target_tree: 'Tree') -> float:
|
|
358
|
+
"""Calculate Point Basal Area in Larger trees (PBAL).
|
|
359
|
+
|
|
360
|
+
PBAL is the basal area of trees with DBH larger than the target tree.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
trees: List of all Tree objects in the stand
|
|
364
|
+
target_tree: Tree to calculate PBAL for
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
PBAL in square feet per acre
|
|
368
|
+
"""
|
|
369
|
+
target_dbh = target_tree.dbh
|
|
370
|
+
pbal = sum(
|
|
371
|
+
math.pi * (tree.dbh / 24.0) ** 2
|
|
372
|
+
for tree in trees
|
|
373
|
+
if tree.dbh > target_dbh
|
|
374
|
+
)
|
|
375
|
+
return pbal
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# Module-level convenience functions for backwards compatibility
|
|
379
|
+
_default_calculator: Optional[StandMetricsCalculator] = None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_metrics_calculator(species: str = 'LP') -> StandMetricsCalculator:
|
|
383
|
+
"""Get or create a metrics calculator instance.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
species: Default species code
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
StandMetricsCalculator instance
|
|
390
|
+
"""
|
|
391
|
+
global _default_calculator
|
|
392
|
+
if _default_calculator is None:
|
|
393
|
+
_default_calculator = StandMetricsCalculator(species)
|
|
394
|
+
return _default_calculator
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def calculate_stand_ccf(trees: List['Tree']) -> float:
|
|
398
|
+
"""Calculate CCF for a list of trees.
|
|
399
|
+
|
|
400
|
+
Convenience function that uses the default calculator.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
trees: List of Tree objects
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Crown Competition Factor
|
|
407
|
+
"""
|
|
408
|
+
return get_metrics_calculator().calculate_ccf(trees)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def calculate_stand_sdi(trees: List['Tree']) -> float:
|
|
412
|
+
"""Calculate SDI for a list of trees.
|
|
413
|
+
|
|
414
|
+
Convenience function that uses the default calculator.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
trees: List of Tree objects
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Stand Density Index
|
|
421
|
+
"""
|
|
422
|
+
return get_metrics_calculator().calculate_sdi(trees)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def calculate_stand_basal_area(trees: List['Tree']) -> float:
|
|
426
|
+
"""Calculate basal area for a list of trees.
|
|
427
|
+
|
|
428
|
+
Convenience function that uses the default calculator.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
trees: List of Tree objects
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Basal area in sq ft/acre
|
|
435
|
+
"""
|
|
436
|
+
return get_metrics_calculator().calculate_basal_area(trees)
|