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.
Files changed (149) hide show
  1. fvs_python-0.2.3.dist-info/METADATA +254 -0
  2. fvs_python-0.2.3.dist-info/RECORD +149 -0
  3. fvs_python-0.2.3.dist-info/WHEEL +5 -0
  4. fvs_python-0.2.3.dist-info/licenses/LICENSE +21 -0
  5. fvs_python-0.2.3.dist-info/top_level.txt +1 -0
  6. pyfvs/__init__.py +107 -0
  7. pyfvs/bark_ratio.py +323 -0
  8. pyfvs/cfg/CFG_README.md +73 -0
  9. pyfvs/cfg/dbh_bounding_table_4_7_1_8.json +103 -0
  10. pyfvs/cfg/ecounit_coefficients_table_4_7_1_5.json +981 -0
  11. pyfvs/cfg/ecounit_coefficients_table_4_7_1_6.json +856 -0
  12. pyfvs/cfg/forest_type_mapping_table_4_7_1_4.json +38 -0
  13. pyfvs/cfg/fortype_coefficients_table_4_7_1_3.json +1183 -0
  14. pyfvs/cfg/functional_forms.yaml +111 -0
  15. pyfvs/cfg/growth_model_parameters.yaml +98 -0
  16. pyfvs/cfg/plant_values_table_4_7_1_7.json +15 -0
  17. pyfvs/cfg/site_index_transformation.yaml +113 -0
  18. pyfvs/cfg/sn_bark_ratio_coefficients.json +115 -0
  19. pyfvs/cfg/sn_crown_competition_factor.json +109 -0
  20. pyfvs/cfg/sn_crown_ratio_coefficients.json +956 -0
  21. pyfvs/cfg/sn_crown_width_coefficients.json +1664 -0
  22. pyfvs/cfg/sn_diameter_growth_coefficients.json +300 -0
  23. pyfvs/cfg/sn_height_diameter_coefficients.json +97 -0
  24. pyfvs/cfg/sn_large_tree_diameter_growth.json +191 -0
  25. pyfvs/cfg/sn_large_tree_height_growth.json +229 -0
  26. pyfvs/cfg/sn_large_tree_height_growth_coefficients.json +1187 -0
  27. pyfvs/cfg/sn_mortality_model.json +176 -0
  28. pyfvs/cfg/sn_regeneration_model.json +252 -0
  29. pyfvs/cfg/sn_relative_site_index.json +263 -0
  30. pyfvs/cfg/sn_small_tree_height_growth.json +879 -0
  31. pyfvs/cfg/sn_species_codes_table.json +728 -0
  32. pyfvs/cfg/sn_stand_density_index.json +398 -0
  33. pyfvs/cfg/species/ab_american_basswood.yaml +251 -0
  34. pyfvs/cfg/species/ae_american_elm.yaml +240 -0
  35. pyfvs/cfg/species/ah_american_hornbeam.yaml +250 -0
  36. pyfvs/cfg/species/ap_american_plum.yaml +251 -0
  37. pyfvs/cfg/species/as_american_sycamore.yaml +253 -0
  38. pyfvs/cfg/species/ba_black_ash.yaml +254 -0
  39. pyfvs/cfg/species/bb_basswood.yaml +254 -0
  40. pyfvs/cfg/species/bc_black_cherry.yaml +254 -0
  41. pyfvs/cfg/species/bd_sweet_birch.yaml +252 -0
  42. pyfvs/cfg/species/be_american_beech.yaml +251 -0
  43. pyfvs/cfg/species/bg_black_gum.yaml +252 -0
  44. pyfvs/cfg/species/bj_blue_jay.yaml +254 -0
  45. pyfvs/cfg/species/bk_sugar_maple.yaml +251 -0
  46. pyfvs/cfg/species/bn_butternut.yaml +252 -0
  47. pyfvs/cfg/species/bo_red_maple.yaml +255 -0
  48. pyfvs/cfg/species/bt_bigtooth_aspen.yaml +254 -0
  49. pyfvs/cfg/species/bu_buckeye.yaml +252 -0
  50. pyfvs/cfg/species/by_bald_cypress.yaml +255 -0
  51. pyfvs/cfg/species/ca_american_chestnut.yaml +254 -0
  52. pyfvs/cfg/species/cb_cucumber_tree.yaml +254 -0
  53. pyfvs/cfg/species/ck_virginia_pine.yaml +254 -0
  54. pyfvs/cfg/species/co_pond_cypress.yaml +251 -0
  55. pyfvs/cfg/species/ct_catalpa.yaml +251 -0
  56. pyfvs/cfg/species/cw_chestnut_oak.yaml +253 -0
  57. pyfvs/cfg/species/dw_dogwood.yaml +250 -0
  58. pyfvs/cfg/species/el_american_hornbeam.yaml +254 -0
  59. pyfvs/cfg/species/fm_flowering_dogwood.yaml +251 -0
  60. pyfvs/cfg/species/fr_fraser_fir.yaml +247 -0
  61. pyfvs/cfg/species/ga_green_ash.yaml +254 -0
  62. pyfvs/cfg/species/ha_hawthorn.yaml +252 -0
  63. pyfvs/cfg/species/hb_hornbeam.yaml +254 -0
  64. pyfvs/cfg/species/hh_dogwood.yaml +251 -0
  65. pyfvs/cfg/species/hi_hickory_species.yaml +252 -0
  66. pyfvs/cfg/species/hl_holly.yaml +254 -0
  67. pyfvs/cfg/species/hm_eastern_hemlock.yaml +246 -0
  68. pyfvs/cfg/species/hy_holly.yaml +252 -0
  69. pyfvs/cfg/species/ju_eastern_juniper.yaml +247 -0
  70. pyfvs/cfg/species/lb_loblolly_bay.yaml +254 -0
  71. pyfvs/cfg/species/lk_laurel_oak.yaml +254 -0
  72. pyfvs/cfg/species/ll_longleaf_pine.yaml +265 -0
  73. pyfvs/cfg/species/lo_silver_maple.yaml +252 -0
  74. pyfvs/cfg/species/lp_loblolly_pine.yaml +268 -0
  75. pyfvs/cfg/species/mb_mountain_birch.yaml +250 -0
  76. pyfvs/cfg/species/mg_magnolia.yaml +251 -0
  77. pyfvs/cfg/species/ml_maple_leaf.yaml +254 -0
  78. pyfvs/cfg/species/ms_maple_species.yaml +247 -0
  79. pyfvs/cfg/species/mv_magnolia_vine.yaml +254 -0
  80. pyfvs/cfg/species/oh_other_hardwood.yaml +231 -0
  81. pyfvs/cfg/species/os_other_softwood.yaml +232 -0
  82. pyfvs/cfg/species/ot_other_tree.yaml +210 -0
  83. pyfvs/cfg/species/ov_overcup_oak.yaml +254 -0
  84. pyfvs/cfg/species/pc_pond_cypress.yaml +254 -0
  85. pyfvs/cfg/species/pd_pitch_pine.yaml +245 -0
  86. pyfvs/cfg/species/pi_pine_species.yaml +246 -0
  87. pyfvs/cfg/species/po_american_beech.yaml +254 -0
  88. pyfvs/cfg/species/pp_pond_pine.yaml +246 -0
  89. pyfvs/cfg/species/ps_persimmon.yaml +251 -0
  90. pyfvs/cfg/species/pu_pond_pine.yaml +249 -0
  91. pyfvs/cfg/species/qs_flowering_dogwood.yaml +254 -0
  92. pyfvs/cfg/species/ra_red_ash.yaml +245 -0
  93. pyfvs/cfg/species/rd_redbud.yaml +251 -0
  94. pyfvs/cfg/species/rl_red_elm.yaml +240 -0
  95. pyfvs/cfg/species/rm_red_maple.yaml +256 -0
  96. pyfvs/cfg/species/ro_eastern_hemlock.yaml +255 -0
  97. pyfvs/cfg/species/sa_slash_pine.yaml +265 -0
  98. pyfvs/cfg/species/sb_sweet_birch.yaml +255 -0
  99. pyfvs/cfg/species/sd_sand_pine.yaml +251 -0
  100. pyfvs/cfg/species/sk_swamp_oak.yaml +253 -0
  101. pyfvs/cfg/species/sm_sugar_maple.yaml +252 -0
  102. pyfvs/cfg/species/sn_loblolly_pine.yaml +254 -0
  103. pyfvs/cfg/species/so_southern_oak.yaml +253 -0
  104. pyfvs/cfg/species/sp_shortleaf_pine.yaml +267 -0
  105. pyfvs/cfg/species/sr_spruce_pine.yaml +246 -0
  106. pyfvs/cfg/species/ss_basswood.yaml +251 -0
  107. pyfvs/cfg/species/su_sweetgum.yaml +255 -0
  108. pyfvs/cfg/species/sv_silver_maple.yaml +255 -0
  109. pyfvs/cfg/species/sy_sycamore.yaml +254 -0
  110. pyfvs/cfg/species/tm_tamarack.yaml +246 -0
  111. pyfvs/cfg/species/to_tulip_oak.yaml +254 -0
  112. pyfvs/cfg/species/ts_tulip_tree.yaml +253 -0
  113. pyfvs/cfg/species/vp_virginia_pine.yaml +248 -0
  114. pyfvs/cfg/species/wa_white_ash.yaml +254 -0
  115. pyfvs/cfg/species/we_white_elm.yaml +250 -0
  116. pyfvs/cfg/species/wi_willow.yaml +248 -0
  117. pyfvs/cfg/species/wk_water_oak.yaml +254 -0
  118. pyfvs/cfg/species/wn_walnut.yaml +254 -0
  119. pyfvs/cfg/species/wo_white_oak.yaml +256 -0
  120. pyfvs/cfg/species/wp_white_pine.yaml +250 -0
  121. pyfvs/cfg/species/wt_water_tupelo.yaml +254 -0
  122. pyfvs/cfg/species/yp_yellow_poplar.yaml +261 -0
  123. pyfvs/cfg/species_config.yaml +106 -0
  124. pyfvs/clark_profile.py +323 -0
  125. pyfvs/competition.py +332 -0
  126. pyfvs/config_loader.py +375 -0
  127. pyfvs/crown_competition_factor.py +464 -0
  128. pyfvs/crown_ratio.py +377 -0
  129. pyfvs/crown_width.py +512 -0
  130. pyfvs/data_export.py +356 -0
  131. pyfvs/ecological_unit.py +272 -0
  132. pyfvs/exceptions.py +86 -0
  133. pyfvs/fia_integration.py +876 -0
  134. pyfvs/forest_type.py +253 -0
  135. pyfvs/growth_plots.py +579 -0
  136. pyfvs/harvest.py +603 -0
  137. pyfvs/height_diameter.py +248 -0
  138. pyfvs/large_tree_height_growth.py +822 -0
  139. pyfvs/logging_config.py +213 -0
  140. pyfvs/main.py +99 -0
  141. pyfvs/mortality.py +431 -0
  142. pyfvs/parameters.py +121 -0
  143. pyfvs/simulation_engine.py +386 -0
  144. pyfvs/stand.py +1004 -0
  145. pyfvs/stand_metrics.py +436 -0
  146. pyfvs/stand_output.py +552 -0
  147. pyfvs/tree.py +756 -0
  148. pyfvs/validation.py +190 -0
  149. 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)