malcolmSQ commited on
Commit
85ccbbb
·
1 Parent(s): efb9a96

Update allometric equations with corrected coefficients - R. racemosa: TB = 2.0738 · (D²H)^0.67628 (was 1.938) - A. germinans: TB = 1.5595 · (D²H)^0.55864 (was 1.486) - Updated allometry.py, config files, dashboard documentation, and tests

Browse files
configs/declining_increment.yaml CHANGED
@@ -24,7 +24,7 @@ species:
24
  r0: 0.43 # m/yr
25
  T_m: 40 # years
26
  allometry:
27
- equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
28
  initial_values:
29
  dbh: 0.1 # cm
30
  height: 0.2 # m
@@ -51,7 +51,7 @@ species:
51
  r0: 0.46 # m/yr
52
  T_m: 40
53
  allometry:
54
- equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
55
  initial_values:
56
  dbh: 0.1 # cm
57
  height: 0.2 # m
 
24
  r0: 0.43 # m/yr
25
  T_m: 40 # years
26
  allometry:
27
+ equation: "Zanvo et al. 2023: Total = 2.0738 × (DBH² H)^0.67628"
28
  initial_values:
29
  dbh: 0.1 # cm
30
  height: 0.2 # m
 
51
  r0: 0.46 # m/yr
52
  T_m: 40
53
  allometry:
54
+ equation: "Zanvo et al. 2023: Total = 1.5595 × (DBH² H)^0.55864"
55
  initial_values:
56
  dbh: 0.1 # cm
57
  height: 0.2 # m
dashboard/app.py CHANGED
@@ -274,13 +274,13 @@ $$
274
  The model follows these steps to estimate net CO2 emission reductions:
275
 
276
  **1. Biomass Calculation:** Total above-ground and below-ground biomass per tree is calculated using allometric equations, which typically relate DBH and Height to biomass. For example, using equations from Zanvo et al. (2023):
277
- - For *Rhizophora spp.* (Species A):
278
  $$
279
- \mathrm{Biomass}_{\mathrm{total}} = 1.938 \times (\mathrm{DBH}^2 \cdot H)^{0.67628}
280
  $$
281
  - For *Avicennia germinans* (Species B):
282
  $$
283
- \mathrm{Biomass}_{\mathrm{total}} = 1.486 \times (\mathrm{DBH}^2 \cdot H)^{0.55864}
284
  $$
285
  *(Note: The specific equations are defined in the model configuration.)*
286
 
 
274
  The model follows these steps to estimate net CO2 emission reductions:
275
 
276
  **1. Biomass Calculation:** Total above-ground and below-ground biomass per tree is calculated using allometric equations, which typically relate DBH and Height to biomass. For example, using equations from Zanvo et al. (2023):
277
+ - For *Rhizophora racemosa* (Species A):
278
  $$
279
+ \mathrm{Biomass}_{\mathrm{total}} = 2.0738 \times (\mathrm{DBH}^2 \cdot H)^{0.67628}
280
  $$
281
  - For *Avicennia germinans* (Species B):
282
  $$
283
+ \mathrm{Biomass}_{\mathrm{total}} = 1.5595 \times (\mathrm{DBH}^2 \cdot H)^{0.55864}
284
  $$
285
  *(Note: The specific equations are defined in the model configuration.)*
286
 
er_model_core/allometry.py CHANGED
@@ -20,13 +20,13 @@ def calculate_biomass(dbh: float, height: float, species_name: str, params: Dict
20
  base = dbh**2 * height
21
  if base <= 0:
22
  print(f"[ERROR] Non-positive base in calculate_biomass: dbh={dbh}, height={height}, base={base}, species={species_name}")
23
- # Use Zanvo et al. 2023 equations that include both DBH and height
24
- if species_name == "species_A": # Rhizophora
25
- # Total = 1.938 × (DBH² H)^0.67628
26
- total_biomass = 1.938 * (dbh**2 * height)**0.67628
27
- elif species_name == "species_B": # Avicennia
28
- # Total = 1.486 × (DBH² H)^0.55864
29
- total_biomass = 1.486 * (dbh**2 * height)**0.55864
30
  else:
31
  # Use a conservative generic equation if species not recognized
32
  total_biomass = 1.712 * (dbh**2 * height)**0.61746
 
20
  base = dbh**2 * height
21
  if base <= 0:
22
  print(f"[ERROR] Non-positive base in calculate_biomass: dbh={dbh}, height={height}, base={base}, species={species_name}")
23
+ # Use Zanvo et al. 2023 equations that include both DBH and height (corrected equations)
24
+ if species_name == "species_A": # Rhizophora racemosa
25
+ # Total = 2.0738 × (DBH² H)^0.67628
26
+ total_biomass = 2.0738 * (dbh**2 * height)**0.67628
27
+ elif species_name == "species_B": # Avicennia germinans
28
+ # Total = 1.5595 × (DBH² H)^0.55864
29
+ total_biomass = 1.5595 * (dbh**2 * height)**0.55864
30
  else:
31
  # Use a conservative generic equation if species not recognized
32
  total_biomass = 1.712 * (dbh**2 * height)**0.61746
src/er_model.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core implementation of the Emissions Reduction (ER) model for mangrove projects.
3
+ """
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import yaml
11
+ import warnings
12
+
13
+ from .allometry import calculate_biomass
14
+ from .metrics import calculate_carbon
15
+
16
+ # Growth model imports
17
+ from .growth_models.declining_increment import declining_increment_growth, continuous_declining_increment_growth
18
+
19
+
20
+ @dataclass
21
+ class Species:
22
+ """Species-specific parameters for growth and carbon calculations."""
23
+ name: str
24
+ planting_density: float
25
+ # Old style
26
+ mortality_rates: Optional[Dict[str, float]] = None
27
+ # New style
28
+ m_ref: Optional[float] = None
29
+ DBH_ref: Optional[float] = None
30
+ p: Optional[float] = None
31
+ chapman_richards: Dict[str, Dict[str, float]] = None
32
+ allometry: Dict[str, float] = None
33
+ initial_values: Dict[str, float] = None
34
+ linear: Optional[Dict[str, Dict[str, float]]] = None
35
+ linear_plateau: Optional[Dict[str, Dict[str, float]]] = None
36
+ declining_increment: Optional[Dict[str, Dict[str, float]]] = None
37
+
38
+
39
+ @dataclass
40
+ class ProjectConfig:
41
+ """Project configuration parameters."""
42
+ duration_years: int
43
+ planting_schedule: Dict[str, float]
44
+
45
+
46
+ @dataclass
47
+ class CarbonConfig:
48
+ """Carbon conversion and adjustment parameters."""
49
+ biomass_to_carbon: float
50
+ carbon_to_co2: float
51
+ buffer_percentage: float
52
+ leakage_percentage: float
53
+ baseline_emissions: float
54
+ soil_carbon_per_ha_per_year: float = 0.0
55
+
56
+
57
+ class ERModel:
58
+ """
59
+ Emissions Reduction Model for mangrove projects.
60
+
61
+ Calculates carbon sequestration over time based on tree growth,
62
+ mortality, and carbon conversion factors.
63
+ """
64
+
65
+ def __init__(self, config_path: Path = None, config: dict = None):
66
+ """
67
+ Initialize the model from a YAML configuration file or a config dict.
68
+ Args:
69
+ config_path: Path to the YAML configuration file
70
+ config: Config dict (optional)
71
+ """
72
+ if config is not None:
73
+ cfg = config
74
+ else:
75
+ with open(config_path) as f:
76
+ cfg = yaml.safe_load(f)
77
+ self.species = [Species(**s) for s in cfg["species"]]
78
+ self.project = ProjectConfig(**cfg["project"])
79
+ self.carbon = CarbonConfig(**cfg["carbon"])
80
+ self.results: Optional[pd.DataFrame] = None
81
+ self.species_results: Optional[pd.DataFrame] = None
82
+ self.scenario_results: Optional[pd.DataFrame] = None
83
+ self.growth_model = cfg.get('growth_model', 'chapman_richards')
84
+ self.continuous_growth = cfg.get('continuous_growth', False)
85
+
86
+ def calculate_cohort_surviving_trees(self, planting_year: int, current_year: int, initial_trees: float, species: Species, plateau_density: Optional[float] = None, growth_model: str = None) -> float:
87
+ """
88
+ Calculate surviving trees for a cohort planted in planting_year, in current_year.
89
+ Uses either per-year mortality (from config) or DBH-dependent mortality.
90
+ growth_model: which growth model to use for DBH (e.g., 'linear', 'linear_plateau', etc.)
91
+ """
92
+ if growth_model is None:
93
+ growth_model = getattr(self, 'growth_model', 'chapman_richards')
94
+ age = current_year - planting_year + 1
95
+ if age < 1:
96
+ return 0
97
+ if initial_trees == 0:
98
+ # Suppress debug output for zero-initial-trees cohorts
99
+ return 0
100
+ N_live = initial_trees
101
+ for year in range(1, age + 1):
102
+ debug_info = {
103
+ 'planting_year': planting_year,
104
+ 'current_year': current_year,
105
+ 'cohort_age': year,
106
+ 'initial_trees': initial_trees,
107
+ 'N_live_before': N_live
108
+ }
109
+ if species.mortality_rates is not None:
110
+ year_key = f"year_{year}"
111
+ if year_key in species.mortality_rates:
112
+ mort_rate = species.mortality_rates[year_key]
113
+ else:
114
+ mort_rate = species.mortality_rates.get("subsequent", 0)
115
+ print(f"[DEBUG] Year key '{year_key}' not found in mortality_rates for {species.name}. Using 'subsequent' or 0. Available keys: {list(species.mortality_rates.keys())}")
116
+ m = mort_rate / 100.0
117
+ debug_info['mortality_logic'] = 'annual'
118
+ debug_info['mortality_rate_percent'] = mort_rate
119
+ else:
120
+ growth_func, dbh_params = self.get_growth_function_and_params(species, growth_model, 'dbh')
121
+ dbh = growth_func(year, dbh_params, species.initial_values["dbh"])
122
+ dbh = max(dbh, 1.0)
123
+ m_ref = species.m_ref if species.m_ref is not None else 0.16
124
+ DBH_ref = species.DBH_ref if species.DBH_ref is not None else 9.0
125
+ p = species.p if species.p is not None else 1.493
126
+ m = m_ref * (DBH_ref / dbh) ** p
127
+ m = min(max(m, 0), 0.99)
128
+ debug_info['mortality_logic'] = 'dbh-dependent'
129
+ debug_info['mortality_rate_percent'] = m * 100
130
+ debug_info['dbh'] = dbh
131
+ N_live = N_live * (1 - m)
132
+ debug_info['N_live_after'] = N_live
133
+ print(f"[DEBUG] {species.name} | PlantingYear: {planting_year} | Year: {current_year} | CohortAge: {year} | MortalityLogic: {debug_info['mortality_logic']} | MortalityRate(%): {debug_info['mortality_rate_percent']} | N_live_before: {debug_info['N_live_before']} | N_live_after: {debug_info['N_live_after']}")
134
+ return N_live
135
+
136
+ def calculate_total_surviving_trees(self, year: int) -> Dict[str, float]:
137
+ """
138
+ Calculate total surviving trees for each species in a given year, summing across all cohorts.
139
+ Returns a dict: {species_name: total_surviving_trees}
140
+ """
141
+ growth_model = getattr(self, 'growth_model', 'chapman_richards')
142
+ totals = {}
143
+ for species in self.species:
144
+ total = 0
145
+ for planting_year, area in self.project.planting_schedule.items():
146
+ py = int(planting_year.split("_")[1])
147
+ initial_trees = species.planting_density * area
148
+ # Use plateau_density as the year-5 value for this cohort
149
+ plateau_density = species.planting_density * area if 5 <= (year - py + 1) else None
150
+ total += self.calculate_cohort_surviving_trees(py, year, initial_trees, species, plateau_density, growth_model)
151
+ totals[species.name] = total
152
+ return totals
153
+
154
+ def calculate_cumulative_area(self, year: int) -> float:
155
+ """
156
+ Calculate cumulative area planted up to and including the given year.
157
+ """
158
+ total = 0
159
+ for planting_year, area in self.project.planting_schedule.items():
160
+ py = int(planting_year.split("_")[1])
161
+ if py <= year:
162
+ total += area
163
+ return total
164
+
165
+ @staticmethod
166
+ def chapman_richards_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
167
+ a, b, c = params["a"], params["b"], params["c"]
168
+ return initial_value + (a - initial_value) * (1 - np.exp(-b * age)) ** c
169
+
170
+ @staticmethod
171
+ def linear_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
172
+ r = params["r"]
173
+ return initial_value + r * age
174
+
175
+ @staticmethod
176
+ def linear_plateau_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
177
+ r = params["r"]
178
+ T_p = params["T_p"]
179
+ a = params["a"]
180
+ if age <= T_p:
181
+ return initial_value + r * age
182
+ else:
183
+ return initial_value + a
184
+
185
+ @staticmethod
186
+ def declining_increment_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
187
+ r0 = params["r0"]
188
+ T_m = params["T_m"]
189
+ # Accumulate annual increments, never negative
190
+ total = initial_value
191
+ for i in range(1, int(np.floor(age)) + 1):
192
+ increment = r0 * max(0, 1 - (i - 1) / T_m)
193
+ total += increment
194
+ # If age is fractional, add partial increment for the last year
195
+ frac = age - int(np.floor(age))
196
+ if frac > 0:
197
+ i = int(np.floor(age)) + 1
198
+ increment = r0 * max(0, 1 - (i - 1) / T_m)
199
+ total += frac * increment
200
+ return total
201
+
202
+ @staticmethod
203
+ def continuous_declining_increment_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
204
+ r0 = params["r0"]
205
+ T_m = params["T_m"]
206
+ # Continuous formula: initial + r0 * (age - age^2/(2*Tm))
207
+ return initial_value + r0 * (age - age**2 / (2 * T_m))
208
+
209
+ def calculate_carbon_for_species(self, species: Species, age: int, area: float, cohort_age: int) -> float:
210
+ """
211
+ Calculate carbon sequestration for a single species, cohort, and cohort age.
212
+ Args:
213
+ species: Species parameters
214
+ age: Project year (not used for growth)
215
+ area: Planted area in hectares
216
+ cohort_age: Age of this cohort (years since planting)
217
+ Returns:
218
+ Carbon sequestration in tCO2
219
+ """
220
+ if cohort_age < 1:
221
+ return 0
222
+ initial_trees = species.planting_density * area
223
+ plateau_density = species.planting_density * area if cohort_age >= 5 else None
224
+ surviving = self.calculate_cohort_surviving_trees(1, cohort_age, initial_trees, species, plateau_density, self.growth_model)
225
+ dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
226
+ height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
227
+ dbh = dbh_func(cohort_age, dbh_params, species.initial_values["dbh"])
228
+ height = height_func(cohort_age, height_params, species.initial_values["height"])
229
+ biomass = calculate_biomass(dbh, height, species.name, species.allometry)
230
+ carbon = calculate_carbon(
231
+ biomass * surviving,
232
+ self.carbon.biomass_to_carbon,
233
+ self.carbon.carbon_to_co2
234
+ )
235
+ return carbon
236
+
237
+ def run(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
238
+ """
239
+ Execute the full ER calculation pipeline.
240
+ Returns:
241
+ Tuple of (yearly results DataFrame, species results DataFrame)
242
+ """
243
+ years = range(1, self.project.duration_years + 1)
244
+ results = []
245
+ species_results = []
246
+ species_metrics_rows = [] # For new per-year, per-species metrics
247
+ for year in years:
248
+ year_results = {"year": year}
249
+ species_year_results = {"Year": year}
250
+ total_carbon = 0
251
+ cumulative_area = self.calculate_cumulative_area(year)
252
+ for species in self.species:
253
+ species_carbon = 0
254
+ # --- New metrics ---
255
+ total_surviving = 0
256
+ total_dbh = 0
257
+ total_height = 0
258
+ total_biomass_per_tree = 0
259
+ total_biomass = 0
260
+ n_cohorts = 0
261
+ for planting_year, area in self.project.planting_schedule.items():
262
+ py = int(planting_year.split("_")[1])
263
+ cohort_age = year - py + 1
264
+ if cohort_age < 1:
265
+ continue
266
+ initial_trees = species.planting_density * area
267
+ plateau_density = species.planting_density * area if cohort_age >= 5 else None
268
+ surviving = self.calculate_cohort_surviving_trees(1, cohort_age, initial_trees, species, plateau_density, self.growth_model)
269
+ dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
270
+ height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
271
+ dbh = dbh_func(cohort_age, dbh_params, species.initial_values["dbh"])
272
+ height = height_func(cohort_age, height_params, species.initial_values["height"])
273
+ biomass_per_tree = calculate_biomass(dbh, height, species.name, species.allometry)
274
+ total_surviving += surviving
275
+ total_dbh += dbh * surviving
276
+ total_height += height * surviving
277
+ total_biomass_per_tree += biomass_per_tree * surviving
278
+ total_biomass += biomass_per_tree * surviving
279
+ n_cohorts += surviving
280
+ # --- End new metrics ---
281
+ # Existing carbon calculation
282
+ carbon = self.calculate_carbon_for_species(species, year, area, cohort_age)
283
+ species_carbon += carbon
284
+ total_carbon += species_carbon
285
+ species_key = f"{species.name} tCO2"
286
+ species_year_results[species_key] = species_carbon
287
+ # Store per-year, per-species metrics
288
+ if total_surviving > 0:
289
+ avg_dbh = total_dbh / total_surviving
290
+ avg_height = total_height / total_surviving
291
+ avg_biomass_per_tree = total_biomass_per_tree / total_surviving
292
+ else:
293
+ avg_dbh = 0
294
+ avg_height = 0
295
+ avg_biomass_per_tree = 0
296
+ species_metrics_rows.append({
297
+ "Year": year,
298
+ "Species": species.name,
299
+ "Surviving Trees": total_surviving,
300
+ "DBH (cm)": avg_dbh,
301
+ "Height (m)": avg_height,
302
+ "Biomass per Tree (kg)": avg_biomass_per_tree,
303
+ "Total Biomass (kg)": total_biomass
304
+ })
305
+ species_year_results["Total tCO2"] = total_carbon
306
+ species_year_results["Cumulative ha"] = cumulative_area
307
+ species_year_results["tCO2/ha"] = total_carbon / cumulative_area if cumulative_area > 0 else 0
308
+ gross_carbon = total_carbon
309
+ buffer_carbon = gross_carbon * (1 - self.carbon.buffer_percentage / 100)
310
+ buffer_carbon -= self.carbon.leakage_percentage / 100 * gross_carbon
311
+ buffer_carbon -= self.carbon.baseline_emissions * cumulative_area
312
+ # Cumulative soil carbon: add 1 t/ha for every hectare ever planted, each year
313
+ soil_carbon = 0
314
+ if hasattr(self.carbon, 'soil_carbon_per_ha_per_year'):
315
+ soil_carbon = self.carbon.soil_carbon_per_ha_per_year * cumulative_area
316
+ gross_carbon_with_soil = gross_carbon + soil_carbon
317
+ buffer_carbon_with_soil = buffer_carbon + soil_carbon
318
+ year_results.update({
319
+ "gross_carbon": gross_carbon,
320
+ "buffer_carbon": buffer_carbon,
321
+ "cumulative_area": cumulative_area,
322
+ "gross_carbon_with_soil": gross_carbon_with_soil,
323
+ "buffer_carbon_with_soil": buffer_carbon_with_soil,
324
+ "soil_carbon": soil_carbon
325
+ })
326
+ results.append(year_results)
327
+ species_results.append(species_year_results)
328
+ self.results = pd.DataFrame(results)
329
+ self.species_results = pd.DataFrame(species_results)
330
+ self.species_metrics = pd.DataFrame(species_metrics_rows)
331
+ return self.results, self.species_results
332
+
333
+ def save_results(self, output_path: Path) -> None:
334
+ """
335
+ Save results to CSV file.
336
+
337
+ Args:
338
+ output_path: Path to save the results CSV
339
+ """
340
+ if self.results is None:
341
+ raise ValueError("No results available. Run the model first.")
342
+ self.results.to_csv(output_path, index=False)
343
+
344
+ def get_growth_function_and_params(self, species, growth_model, dim):
345
+ """
346
+ Returns the correct growth function and parameter dict for the given species and dimension (dbh or height).
347
+ """
348
+ if growth_model == "linear":
349
+ # from growth_models.linear import linear_growth
350
+ func = None # ARCHIVED/NOT IN USE
351
+ params = species.linear[dim]
352
+ elif growth_model == "linear_plateau":
353
+ # from growth_models.linear import linear_plateau_growth
354
+ func = None # ARCHIVED/NOT IN USE
355
+ params = species.linear_plateau[dim]
356
+ elif growth_model == "declining_increment":
357
+ if getattr(self, 'continuous_growth', False):
358
+ func = continuous_declining_increment_growth
359
+ else:
360
+ func = declining_increment_growth
361
+ params = species.declining_increment[dim]
362
+ else:
363
+ # from growth_models.chapman_richards import chapman_richards_growth
364
+ func = None # ARCHIVED/NOT IN USE
365
+ params = species.chapman_richards[dim]
366
+ return func, params
367
+
368
+ # --- Parameter sweep/test for plausible survival curves ---
369
+ def test_dbh_mortality_sweep():
370
+ import matplotlib.pyplot as plt
371
+ import numpy as np
372
+ m_refs = [0.01, 0.05, 0.1, 0.16]
373
+ ps = [1.0, 1.5, 2.0]
374
+ DBH_ref = 9.0
375
+ years = np.arange(1, 31)
376
+ initial_trees = 1000
377
+ results = {}
378
+ for m_ref in m_refs:
379
+ for p in ps:
380
+ N_live = initial_trees
381
+ N_lives = []
382
+ for year in years:
383
+ dbh = 1.0 + (year - 1) * 0.5 # simple linear DBH growth for test
384
+ dbh = max(dbh, 1.0)
385
+ m = m_ref * (DBH_ref / dbh) ** p
386
+ m = min(max(m, 0), 0.99)
387
+ N_live = N_live * (1 - m)
388
+ N_lives.append(N_live)
389
+ results[(m_ref, p)] = N_lives
390
+ plt.figure(figsize=(10,6))
391
+ for (m_ref, p), N_lives in results.items():
392
+ plt.plot(years, N_lives, label=f"m_ref={m_ref}, p={p}")
393
+ plt.xlabel("Year")
394
+ plt.ylabel("Surviving Trees")
395
+ plt.title("DBH-dependent Mortality Parameter Sweep")
396
+ plt.legend()
397
+ plt.grid(True)
398
+ plt.show()
399
+
400
+ # To run the test, call test_dbh_mortality_sweep() from __main__ or a notebook.
tests/test_er_model.py CHANGED
@@ -176,29 +176,14 @@ def test_allometric_equation_species_A_B():
176
  # Test values
177
  dbh = 10.0 # cm
178
  height = 5.0 # m
179
- # species_A (Rhizophora spp.)
180
- expected_A = 1.938 * (dbh**2 * height)**0.67628
181
  result_A = calculate_biomass(dbh, height, "species_A", {})
182
  assert np.isclose(result_A, expected_A, rtol=1e-6), f"species_A: got {result_A}, expected {expected_A}"
183
  # species_B (Avicennia germinans)
184
- expected_B = 1.486 * (dbh**2 * height)**0.55864
185
  result_B = calculate_biomass(dbh, height, "species_B", {})
186
- assert np.isclose(result_B, expected_B, rtol=1e-6), f"species_B: got {result_B}, expected {expected_B}"
187
-
188
- # Test with a known allometry to check biomass calculation linkage
189
- from er_model_core.allometry import calculate_biomass
190
-
191
- # Basic check that biomass is positive for reasonable inputs
192
- # ... existing code ...
193
-
194
- # Re-import ERModel and Species for this specific test to ensure clean state if needed
195
- from er_model_core.er_model import ERModel, Species
196
- model = ERModel(config=config_dict)
197
- results_df, species_results_df = model.run()
198
-
199
- # ... existing code ...
200
-
201
- # ... existing code ...
202
 
203
  # --- Parameter sweep/test for plausible survival curves (moved from er_model.py) ---
204
  def test_dbh_mortality_sweep():
 
176
  # Test values
177
  dbh = 10.0 # cm
178
  height = 5.0 # m
179
+ # species_A (Rhizophora racemosa)
180
+ expected_A = 2.0738 * (dbh**2 * height)**0.67628
181
  result_A = calculate_biomass(dbh, height, "species_A", {})
182
  assert np.isclose(result_A, expected_A, rtol=1e-6), f"species_A: got {result_A}, expected {expected_A}"
183
  # species_B (Avicennia germinans)
184
+ expected_B = 1.5595 * (dbh**2 * height)**0.55864
185
  result_B = calculate_biomass(dbh, height, "species_B", {})
186
+ assert np.isclose(result_B, expected_B, rtol=1e-6), f"species_B: got {result_B}, expected {expected_B}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  # --- Parameter sweep/test for plausible survival curves (moved from er_model.py) ---
189
  def test_dbh_mortality_sweep():