malcolmSQ commited on
Commit
9047195
·
1 Parent(s): 3f105b6

Dashboard cleanup: remove per-hectare chart, improve table formatting, clarify table names and titles

Browse files
configs/declining_increment.yaml CHANGED
@@ -71,6 +71,7 @@ carbon:
71
  buffer_percentage: 20 # risk buffer
72
  leakage_percentage: 0 # no leakage assumed
73
  baseline_emissions: 0 # tCO2/ha/year
 
74
 
75
  scenarios:
76
  area: 1000.0 # ha
 
71
  buffer_percentage: 20 # risk buffer
72
  leakage_percentage: 0 # no leakage assumed
73
  baseline_emissions: 0 # tCO2/ha/year
74
+ soil_carbon_per_ha_per_year: 1.0 # tCO2/ha/year, added annually
75
 
76
  scenarios:
77
  area: 1000.0 # ha
dashboard/app.py CHANGED
@@ -17,6 +17,8 @@ import plotly.graph_objs as go
17
  from plotly.subplots import make_subplots
18
  import warnings
19
  import traceback
 
 
20
 
21
  MODEL_CONFIGS = {
22
  "Original Model": "configs/params.yaml",
@@ -45,22 +47,24 @@ def create_survival_table(model):
45
  """
46
  years = range(1, model.project.duration_years + 1)
47
  data = {"Year": []}
48
- species_names = [SPECIES_DISPLAY_NAMES.get(s.name, s.name) for s in model.species]
49
- for name in species_names:
 
 
50
  data[name] = []
51
  data["Total Surviving Trees"] = []
52
  for year in years:
53
  data["Year"].append(year)
54
  totals = model.calculate_total_surviving_trees(year)
55
  total = 0
56
- for name in species_names:
57
- val = totals.get(name, 0)
58
- data[name].append(val)
59
  total += val
60
  data["Total Surviving Trees"].append(total)
61
  df = pd.DataFrame(data)
62
  # Format numbers: no decimals, thousands separator
63
- for name in species_names + ["Total Surviving Trees"]:
64
  df[name] = df[name].apply(lambda x: f"{x:,.0f}")
65
  return df
66
 
@@ -116,7 +120,9 @@ def create_growth_increment_plots(config, model_type=None):
116
  """
117
  if model_type is None:
118
  model_type = config.get('growth_model', 'chapman_richards')
119
- years = np.arange(1, config["project"]["duration_years"] + 1)
 
 
120
  fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
121
  from src.er_model import ERModel
122
  use_continuous = config.get('continuous_growth', False)
@@ -125,44 +131,33 @@ def create_growth_increment_plots(config, model_type=None):
125
  initial_dbh = sp["initial_values"]["dbh"]
126
  initial_height = sp["initial_values"]["height"]
127
  if model_type == "linear":
128
- dbh_params = sp["linear"]["dbh"]
129
- height_params = sp["linear"]["height"]
130
- dbh = [ERModel.linear_growth(t, dbh_params, initial_dbh) for t in years]
131
- height = [ERModel.linear_growth(t, height_params, initial_height) for t in years]
132
  elif model_type == "linear_plateau":
133
- dbh_params = sp["linear_plateau"]["dbh"]
134
- height_params = sp["linear_plateau"]["height"]
135
- dbh = [ERModel.linear_plateau_growth(t, dbh_params, initial_dbh) for t in years]
136
- height = [ERModel.linear_plateau_growth(t, height_params, initial_height) for t in years]
137
  elif model_type == "declining_increment":
138
- dbh_params = sp["declining_increment"]["dbh"]
139
- height_params = sp["declining_increment"]["height"]
140
  if use_continuous:
141
- dbh = [ERModel.continuous_declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
142
- height = [ERModel.continuous_declining_increment_growth(t, height_params, initial_height) for t in years]
143
  else:
144
- dbh = [ERModel.declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
145
- height = [ERModel.declining_increment_growth(t, height_params, initial_height) for t in years]
146
- else: # Default to Chapman-Richards
147
- dbh_params = sp["chapman_richards"]["dbh"]
148
- height_params = sp["chapman_richards"]["height"]
149
- def chapman_richards(t, params, initial):
150
- a, b, c = params["a"], params["b"], params["c"]
151
- return initial + (a - initial) * (1 - np.exp(-b * t)) ** c
152
- dbh = [chapman_richards(t, dbh_params, initial_dbh) for t in years]
153
- height = [chapman_richards(t, height_params, initial_height) for t in years]
154
  dbh = np.array(dbh)
155
  height = np.array(height)
156
  check_complex(dbh, f"{name} DBH (growth)")
157
  check_complex(height, f"{name} Height (growth)")
158
- dbh_inc = np.diff(np.insert(dbh, 0, dbh[0]))
159
- height_inc = np.diff(np.insert(height, 0, height[0]))
160
  check_complex(dbh_inc, f"{name} DBH Δ (increment)")
161
  check_complex(height_inc, f"{name} Height Δ (increment)")
162
- fig.add_trace(go.Scatter(x=years, y=dbh, mode='lines', name=f"{name} DBH", legendgroup=name, line=dict(width=2)), row=1, col=1)
163
- fig.add_trace(go.Scatter(x=years, y=height, mode='lines', name=f"{name} Height", legendgroup=name, line=dict(width=2)), row=1, col=2)
164
- fig.add_trace(go.Scatter(x=years, y=dbh_inc, mode='lines', name=f"{name} DBH Δ", legendgroup=name, showlegend=False, line=dict(width=2)), row=2, col=1)
165
- fig.add_trace(go.Scatter(x=years, y=height_inc, mode='lines', name=f"{name} Height Δ", legendgroup=name, showlegend=False, line=dict(width=2)), row=2, col=2)
166
  fig.update_layout(height=700, width=900, title_text="Growth and Increment Curves", hovermode="x unified")
167
  fig.update_xaxes(title_text="Age (years)", row=1, col=1)
168
  fig.update_xaxes(title_text="Age (years)", row=1, col=2)
@@ -179,10 +174,17 @@ def create_all_plots(results, species_results, config):
179
  for col in ["gross_carbon", "net_carbon"]:
180
  arr = np.array(results[col])
181
  check_complex(arr, f"results['{col}']")
182
- # 1. Carbon curve (gross and net)
183
  fig1 = go.Figure()
184
- fig1.add_trace(go.Scatter(x=results["year"], y=results["gross_carbon"], mode="lines+markers", name="Gross Carbon", line=dict(width=2)))
185
- fig1.add_trace(go.Scatter(x=results["year"], y=results["net_carbon"], mode="lines+markers", name="Net Carbon", line=dict(width=2)))
 
 
 
 
 
 
 
186
  fig1.update_layout(title="Carbon Sequestration Over Time", xaxis_title="Year", yaxis_title="Carbon (tCO2)", hovermode="x unified", template="plotly_white")
187
 
188
  # 2. Annual carbon (ERs)
@@ -202,6 +204,7 @@ def create_all_plots(results, species_results, config):
202
  name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
203
  initial_dbh = sp["initial_values"]["dbh"]
204
  initial_height = sp["initial_values"]["height"]
 
205
  if growth_model == "linear":
206
  dbh_params = sp["linear"]["dbh"]
207
  height_params = sp["linear"]["height"]
@@ -215,8 +218,12 @@ def create_all_plots(results, species_results, config):
215
  elif growth_model == "declining_increment":
216
  dbh_params = sp["declining_increment"]["dbh"]
217
  height_params = sp["declining_increment"]["height"]
218
- dbh = [ERModel.declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
219
- height = [ERModel.declining_increment_growth(t, height_params, initial_height) for t in years]
 
 
 
 
220
  else: # Default to Chapman-Richards
221
  dbh_params = sp["chapman_richards"]["dbh"]
222
  height_params = sp["chapman_richards"]["height"]
@@ -241,10 +248,18 @@ def create_summary(results, species_results, config, model):
241
  total_area = sum(config["project"]["planting_schedule"].values())
242
  final_gross = results["gross_carbon"].iloc[-1]
243
  final_net = results["net_carbon"].iloc[-1]
 
 
 
 
244
  years = len(results)
245
  gross_carbon_per_ha = final_gross / total_area if total_area > 0 else 0
246
  net_carbon_per_ha = final_net / total_area if total_area > 0 else 0
 
 
247
  annual_net_per_ha = net_carbon_per_ha / years if years > 0 else 0
 
 
248
  # Milestone years
249
  def get_val(col, idx):
250
  return results[col].iloc[idx] if len(results) > idx else 0
@@ -256,6 +271,8 @@ def create_summary(results, species_results, config, model):
256
  year_20_net = get_val("net_carbon", 19)
257
  avg_annual_gross = final_gross / years if years > 0 else 0
258
  avg_annual_net = final_net / years if years > 0 else 0
 
 
259
  # Surviving trees at milestones
260
  milestones = [5, 10, 20, 30]
261
  survival_lines = []
@@ -272,16 +289,146 @@ def create_summary(results, species_results, config, model):
272
  per_ha_lines.append(f"{SPECIES_DISPLAY_NAMES.get(k, k)}: {per_ha:,.0f} trees/ha")
273
  summary = (
274
  f"Project Overview:\n----------------\nDuration: {years} years\nTotal Area Planted: {total_area:,.0f} ha\nBuffer Pool: {config['carbon']['buffer_percentage']}%\n\n"
275
- f"Carbon Sequestration:\n-------------------\nTotal Gross Carbon (Year {years}): {final_gross:,.0f} tCO2\nTotal Net Carbon (Year {years}): {final_net:,.0f} tCO2\nAverage Annual Gross: {avg_annual_gross:,.0f} tCO2/yr\nAverage Annual Net: {avg_annual_net:,.0f} tCO2/yr\n\n"
276
- f"Milestone Years:\n--------------\nYear 5 Gross: {year_5_gross:,.0f} tCO2\nYear 5 Net: {year_5_net:,.0f} tCO2\nYear 10 Gross: {year_10_gross:,.0f} tCO2\nYear 10 Net: {year_10_net:,.0f} tCO2\nYear 20 Gross: {year_20_gross:,.0f} tCO2\nYear 20 Net: {year_20_net:,.0f} tCO2\n\n"
 
 
277
  "Surviving Trees (Milestones):\n----------------------------\n"
278
  + "\n".join(survival_lines)
279
  + "\n\nPer Hectare Surviving Trees (Final Year):\n----------------------------------------\n"
280
  + "\n".join(per_ha_lines)
281
- + f"\n\nPer Hectare Metrics (Year {years}):\n-----------------------------\nGross Carbon per ha: {gross_carbon_per_ha:,.0f} tCO2/ha\nNet Carbon per ha: {net_carbon_per_ha:,.0f} tCO2/ha\nAverage Annual Net per ha: {annual_net_per_ha:,.2f} tCO2/ha/yr\n"
282
  )
283
  return summary
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  with gr.Blocks() as demo:
286
  gr.Markdown("# Mangrove ER Model Dashboard\nDeclining Increment Model Only")
287
  with gr.Tabs():
@@ -303,10 +450,14 @@ with gr.Blocks() as demo:
303
  carbon_plot = gr.Plot()
304
  biomass_plot = gr.Plot()
305
  annual_plot = gr.Plot()
 
 
306
  summary_box = gr.Textbox(label="Summary", lines=12)
307
  results_box = gr.Dataframe(label="Project Results (Annual)")
308
  species_box = gr.Dataframe(label="Species Results (Annual)")
309
  survival_box = gr.Dataframe(label="Surviving Trees Table")
 
 
310
 
311
  def update_declining_increment(y1, y2, y3, y4, y5):
312
  config = update_planting_schedule(MODEL_CONFIGS["Declining Increment"], [y1, y2, y3, y4, y5])
@@ -320,6 +471,8 @@ with gr.Blocks() as demo:
320
  plots = create_all_plots(results, species_results, config)
321
  summary = create_summary(results, species_results, config, model)
322
  survival_table = create_survival_table(model)
 
 
323
  results_fmt = results.copy()
324
  species_results_fmt = species_results.copy()
325
  for col in results_fmt.columns:
@@ -328,21 +481,23 @@ with gr.Blocks() as demo:
328
  for col in species_results_fmt.columns:
329
  if species_results_fmt[col].dtype in [float, int]:
330
  species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
331
- return plots[0], plots[2], plots[1], summary, results_fmt, species_results_fmt, survival_table
332
 
333
  update_btn.click(
334
  update_declining_increment,
335
  inputs=[year_1, year_2, year_3, year_4, year_5],
336
- outputs=[carbon_plot, biomass_plot, annual_plot, summary_box, results_box, species_box, survival_box]
337
  )
338
 
339
  # Show initial results
340
- c, b, a, summary, r, s, surv = update_declining_increment(2500, 2500, 0, 0, 0)
341
  carbon_plot.value = c
342
  biomass_plot.value = b
343
  annual_plot.value = a
 
344
  summary_box.value = summary
345
  results_box.value = r
346
  species_box.value = s
347
  survival_box.value = surv
 
348
  demo.launch(share=True)
 
17
  from plotly.subplots import make_subplots
18
  import warnings
19
  import traceback
20
+ from src.allometry import calculate_biomass
21
+ import copy
22
 
23
  MODEL_CONFIGS = {
24
  "Original Model": "configs/params.yaml",
 
47
  """
48
  years = range(1, model.project.duration_years + 1)
49
  data = {"Year": []}
50
+ # Use internal names for lookup, but display friendly names
51
+ internal_names = [s.name for s in model.species]
52
+ display_names = [SPECIES_DISPLAY_NAMES.get(s.name, s.name) for s in model.species]
53
+ for name in display_names:
54
  data[name] = []
55
  data["Total Surviving Trees"] = []
56
  for year in years:
57
  data["Year"].append(year)
58
  totals = model.calculate_total_surviving_trees(year)
59
  total = 0
60
+ for internal, display in zip(internal_names, display_names):
61
+ val = totals.get(internal, 0)
62
+ data[display].append(val)
63
  total += val
64
  data["Total Surviving Trees"].append(total)
65
  df = pd.DataFrame(data)
66
  # Format numbers: no decimals, thousands separator
67
+ for name in display_names + ["Total Surviving Trees"]:
68
  df[name] = df[name].apply(lambda x: f"{x:,.0f}")
69
  return df
70
 
 
120
  """
121
  if model_type is None:
122
  model_type = config.get('growth_model', 'chapman_richards')
123
+ N = config["project"]["duration_years"]
124
+ ages = np.arange(0, N + 1) # 0 to N
125
+ ages_inc = np.arange(1, N + 1) # 1 to N
126
  fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
127
  from src.er_model import ERModel
128
  use_continuous = config.get('continuous_growth', False)
 
131
  initial_dbh = sp["initial_values"]["dbh"]
132
  initial_height = sp["initial_values"]["height"]
133
  if model_type == "linear":
134
+ dbh = [ERModel.linear_growth(t, sp["linear"]["dbh"], initial_dbh) for t in ages]
135
+ height = [ERModel.linear_growth(t, sp["linear"]["height"], initial_height) for t in ages]
 
 
136
  elif model_type == "linear_plateau":
137
+ dbh = [ERModel.linear_plateau_growth(t, sp["linear_plateau"]["dbh"], initial_dbh) for t in ages]
138
+ height = [ERModel.linear_plateau_growth(t, sp["linear_plateau"]["height"], initial_height) for t in ages]
 
 
139
  elif model_type == "declining_increment":
 
 
140
  if use_continuous:
141
+ dbh = [ERModel.continuous_declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
142
+ height = [ERModel.continuous_declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
143
  else:
144
+ dbh = [ERModel.declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
145
+ height = [ERModel.declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
146
+ else:
147
+ dbh = [ERModel.chapman_richards_growth(t, sp["chapman_richards"]["dbh"], initial_dbh) for t in ages]
148
+ height = [ERModel.chapman_richards_growth(t, sp["chapman_richards"]["height"], initial_height) for t in ages]
 
 
 
 
 
149
  dbh = np.array(dbh)
150
  height = np.array(height)
151
  check_complex(dbh, f"{name} DBH (growth)")
152
  check_complex(height, f"{name} Height (growth)")
153
+ dbh_inc = dbh[1:] - dbh[:-1]
154
+ height_inc = height[1:] - height[:-1]
155
  check_complex(dbh_inc, f"{name} DBH Δ (increment)")
156
  check_complex(height_inc, f"{name} Height Δ (increment)")
157
+ fig.add_trace(go.Scatter(x=ages, y=dbh, mode='lines', name=f"{name} DBH", legendgroup=name, line=dict(width=2)), row=1, col=1)
158
+ fig.add_trace(go.Scatter(x=ages, y=height, mode='lines', name=f"{name} Height", legendgroup=name, line=dict(width=2)), row=1, col=2)
159
+ fig.add_trace(go.Scatter(x=ages_inc, y=dbh_inc, mode='lines', name=f"{name} DBH Δ", legendgroup=name, showlegend=False, line=dict(width=2)), row=2, col=1)
160
+ fig.add_trace(go.Scatter(x=ages_inc, y=height_inc, mode='lines', name=f"{name} Height Δ", legendgroup=name, showlegend=False, line=dict(width=2)), row=2, col=2)
161
  fig.update_layout(height=700, width=900, title_text="Growth and Increment Curves", hovermode="x unified")
162
  fig.update_xaxes(title_text="Age (years)", row=1, col=1)
163
  fig.update_xaxes(title_text="Age (years)", row=1, col=2)
 
174
  for col in ["gross_carbon", "net_carbon"]:
175
  arr = np.array(results[col])
176
  check_complex(arr, f"results['{col}']")
177
+ # 1. Carbon curve (gross and net, with and without soil)
178
  fig1 = go.Figure()
179
+ fig1.add_trace(go.Scatter(x=results["year"], y=results["gross_carbon"], mode="lines+markers", name="Gross Carbon", line=dict(width=2, color="blue")))
180
+ fig1.add_trace(go.Scatter(x=results["year"], y=results["net_carbon"], mode="lines+markers", name="Net Carbon", line=dict(width=2, color="red")))
181
+ if "gross_carbon_with_soil" in results.columns:
182
+ # For backward compatibility, but we now sum soil carbon in summary, so recalc here for plot
183
+ soil_cumsum = results["soil_carbon"].cumsum()
184
+ gross_with_soil = results["gross_carbon"] + soil_cumsum
185
+ net_with_soil = results["net_carbon"] + soil_cumsum
186
+ fig1.add_trace(go.Scatter(x=results["year"], y=gross_with_soil, mode="lines+markers", name="Gross Carbon (with Soil)", line=dict(width=2, dash="dot", color="green")))
187
+ fig1.add_trace(go.Scatter(x=results["year"], y=net_with_soil, mode="lines+markers", name="Net Carbon (with Soil)", line=dict(width=2, dash="dot", color="orange")))
188
  fig1.update_layout(title="Carbon Sequestration Over Time", xaxis_title="Year", yaxis_title="Carbon (tCO2)", hovermode="x unified", template="plotly_white")
189
 
190
  # 2. Annual carbon (ERs)
 
204
  name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
205
  initial_dbh = sp["initial_values"]["dbh"]
206
  initial_height = sp["initial_values"]["height"]
207
+ use_continuous = config.get('continuous_growth', False)
208
  if growth_model == "linear":
209
  dbh_params = sp["linear"]["dbh"]
210
  height_params = sp["linear"]["height"]
 
218
  elif growth_model == "declining_increment":
219
  dbh_params = sp["declining_increment"]["dbh"]
220
  height_params = sp["declining_increment"]["height"]
221
+ if use_continuous:
222
+ dbh = [ERModel.continuous_declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
223
+ height = [ERModel.continuous_declining_increment_growth(t, height_params, initial_height) for t in years]
224
+ else:
225
+ dbh = [ERModel.declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
226
+ height = [ERModel.declining_increment_growth(t, height_params, initial_height) for t in years]
227
  else: # Default to Chapman-Richards
228
  dbh_params = sp["chapman_richards"]["dbh"]
229
  height_params = sp["chapman_richards"]["height"]
 
248
  total_area = sum(config["project"]["planting_schedule"].values())
249
  final_gross = results["gross_carbon"].iloc[-1]
250
  final_net = results["net_carbon"].iloc[-1]
251
+ # Sum soil carbon over all years
252
+ total_soil_carbon = results["soil_carbon"].sum()
253
+ final_gross_soil = final_gross + total_soil_carbon
254
+ final_net_soil = final_net + total_soil_carbon
255
  years = len(results)
256
  gross_carbon_per_ha = final_gross / total_area if total_area > 0 else 0
257
  net_carbon_per_ha = final_net / total_area if total_area > 0 else 0
258
+ gross_carbon_per_ha_soil = final_gross_soil / total_area if total_area > 0 else 0
259
+ net_carbon_per_ha_soil = final_net_soil / total_area if total_area > 0 else 0
260
  annual_net_per_ha = net_carbon_per_ha / years if years > 0 else 0
261
+ annual_net_per_ha_soil = net_carbon_per_ha_soil / years if years > 0 else 0
262
+ soil_carbon_val = config["carbon"].get("soil_carbon_per_ha_per_year", 0)
263
  # Milestone years
264
  def get_val(col, idx):
265
  return results[col].iloc[idx] if len(results) > idx else 0
 
271
  year_20_net = get_val("net_carbon", 19)
272
  avg_annual_gross = final_gross / years if years > 0 else 0
273
  avg_annual_net = final_net / years if years > 0 else 0
274
+ avg_annual_gross_soil = final_gross_soil / years if years > 0 else 0
275
+ avg_annual_net_soil = final_net_soil / years if years > 0 else 0
276
  # Surviving trees at milestones
277
  milestones = [5, 10, 20, 30]
278
  survival_lines = []
 
289
  per_ha_lines.append(f"{SPECIES_DISPLAY_NAMES.get(k, k)}: {per_ha:,.0f} trees/ha")
290
  summary = (
291
  f"Project Overview:\n----------------\nDuration: {years} years\nTotal Area Planted: {total_area:,.0f} ha\nBuffer Pool: {config['carbon']['buffer_percentage']}%\n\n"
292
+ f"Soil Carbon:\n-----------\nSoil carbon added: {soil_carbon_val} tCO2/ha/year\nTotal soil carbon added over project: {total_soil_carbon:,.0f} tCO2\n\n"
293
+ f"Carbon Sequestration (Biomass Only):\n-------------------\nTotal Gross Carbon (Year {years}): {final_gross:,.0f} tCO2\nTotal Net Carbon (Year {years}): {final_net:,.0f} tCO2\nAverage Annual Gross: {avg_annual_gross:,.0f} tCO2/yr\nAverage Annual Net: {avg_annual_net:,.0f} tCO2/yr\n\n"
294
+ f"Carbon Sequestration (With Soil Carbon):\n-------------------\nTotal Gross Carbon (Year {years}): {final_gross_soil:,.0f} tCO2\nTotal Net Carbon (Year {years}): {final_net_soil:,.0f} tCO2\nAverage Annual Gross: {avg_annual_gross_soil:,.0f} tCO2/yr\nAverage Annual Net: {avg_annual_net_soil:,.0f} tCO2/yr\n\n"
295
+ f"Milestone Years (Biomass Only):\n--------------\nYear 5 Gross: {year_5_gross:,.0f} tCO2\nYear 5 Net: {year_5_net:,.0f} tCO2\nYear 10 Gross: {year_10_gross:,.0f} tCO2\nYear 10 Net: {year_10_net:,.0f} tCO2\nYear 20 Gross: {year_20_gross:,.0f} tCO2\nYear 20 Net: {year_20_net:,.0f} tCO2\n\n"
296
  "Surviving Trees (Milestones):\n----------------------------\n"
297
  + "\n".join(survival_lines)
298
  + "\n\nPer Hectare Surviving Trees (Final Year):\n----------------------------------------\n"
299
  + "\n".join(per_ha_lines)
300
+ + f"\n\nPer Hectare Metrics (Year {years}):\n-----------------------------\nGross Carbon per ha (biomass only): {gross_carbon_per_ha:,.0f} tCO2/ha\nNet Carbon per ha (biomass only): {net_carbon_per_ha:,.0f} tCO2/ha\nGross Carbon per ha (with soil): {gross_carbon_per_ha_soil:,.0f} tCO2/ha\nNet Carbon per ha (with soil): {net_carbon_per_ha_soil:,.0f} tCO2/ha\nAverage Annual Net per ha (biomass only): {annual_net_per_ha:,.2f} tCO2/ha/yr\nAverage Annual Net per ha (with soil): {annual_net_per_ha_soil:,.2f} tCO2/ha/yr\n"
301
  )
302
  return summary
303
 
304
+ def create_size_table(config):
305
+ """
306
+ Returns a DataFrame with ages as rows and columns for each species and dimension (dbh, height).
307
+ """
308
+ from src.er_model import ERModel
309
+ ages = range(0, config["project"]["duration_years"] + 1)
310
+ columns = []
311
+ data = {}
312
+ for sp in config["species"]:
313
+ sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
314
+ for dim in ["dbh", "height"]:
315
+ col = (sp_disp, dim)
316
+ columns.append(col)
317
+ data[col] = []
318
+ for age in ages:
319
+ for sp in config["species"]:
320
+ sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
321
+ initial_dbh = sp["initial_values"]["dbh"]
322
+ initial_height = sp["initial_values"]["height"]
323
+ model_type = config.get('growth_model', 'chapman_richards')
324
+ use_continuous = config.get('continuous_growth', False)
325
+ if model_type == "linear":
326
+ dbh = ERModel.linear_growth(age, sp["linear"]["dbh"], initial_dbh)
327
+ height = ERModel.linear_growth(age, sp["linear"]["height"], initial_height)
328
+ elif model_type == "linear_plateau":
329
+ dbh = ERModel.linear_plateau_growth(age, sp["linear_plateau"]["dbh"], initial_dbh)
330
+ height = ERModel.linear_plateau_growth(age, sp["linear_plateau"]["height"], initial_height)
331
+ elif model_type == "declining_increment":
332
+ if use_continuous:
333
+ dbh = ERModel.continuous_declining_increment_growth(age, sp["declining_increment"]["dbh"], initial_dbh)
334
+ height = ERModel.continuous_declining_increment_growth(age, sp["declining_increment"]["height"], initial_height)
335
+ else:
336
+ dbh = ERModel.declining_increment_growth(age, sp["declining_increment"]["dbh"], initial_dbh)
337
+ height = ERModel.declining_increment_growth(age, sp["declining_increment"]["height"], initial_height)
338
+ else:
339
+ dbh = ERModel.chapman_richards_growth(age, sp["chapman_richards"]["dbh"], initial_dbh)
340
+ height = ERModel.chapman_richards_growth(age, sp["chapman_richards"]["height"], initial_height)
341
+ data[(sp_disp, "dbh")].append(round(dbh, 5))
342
+ data[(sp_disp, "height")].append(round(height, 5))
343
+ # Build MultiIndex columns
344
+ columns = pd.MultiIndex.from_tuples(columns, names=["Species", "Dimension"])
345
+ df = pd.DataFrame(data, index=ages)
346
+ df.index.name = "Age"
347
+ df = df[columns] # ensure order
348
+ return df
349
+
350
+ def pretty_table_title(title):
351
+ return f"<span style='font-size:1.3em; font-weight:bold'>{title}</span>"
352
+
353
+ def create_biomass_debug_table(config):
354
+ """
355
+ Returns a DataFrame with years as rows and columns for each species:
356
+ - Surviving Trees
357
+ - DBH (cm)
358
+ - Height (m)
359
+ - Biomass per tree (kg)
360
+ - Total Biomass (kg)
361
+ Uses cohort-level growth (age = year - planting_year + 1).
362
+ """
363
+ from src.er_model import ERModel
364
+ from src.allometry import calculate_biomass
365
+ years = range(1, config["project"]["duration_years"] + 1)
366
+ data = {}
367
+ for sp in config["species"]:
368
+ sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
369
+ data[(sp_disp, "Surviving Trees")] = []
370
+ data[(sp_disp, "DBH (cm)")] = []
371
+ data[(sp_disp, "Height (m)")] = []
372
+ data[(sp_disp, "Biomass per Tree (kg)")] = []
373
+ data[(sp_disp, "Total Biomass (kg)")] = []
374
+ model = ERModel(config=config)
375
+ for year in years:
376
+ for sp in config["species"]:
377
+ sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
378
+ total_surviving = 0
379
+ total_biomass = 0
380
+ total_dbh = 0
381
+ total_height = 0
382
+ total_biomass_per_tree = 0
383
+ n_cohorts = 0
384
+ for planting_year, area in config["project"]["planting_schedule"].items():
385
+ py = int(planting_year.split("_")[1])
386
+ cohort_age = year - py + 1
387
+ if cohort_age < 1:
388
+ continue
389
+ initial_trees = sp["planting_density"] * area
390
+ plateau_density = sp["planting_density"] * area if cohort_age >= 5 else None
391
+ surviving = model.calculate_cohort_surviving_trees(1, cohort_age, initial_trees, model.species[0], plateau_density, model.growth_model)
392
+ dbh_func, dbh_params = model.get_growth_function_and_params(model.species[0], model.growth_model, 'dbh')
393
+ height_func, height_params = model.get_growth_function_and_params(model.species[0], model.growth_model, 'height')
394
+ dbh = dbh_func(cohort_age, dbh_params, sp["initial_values"]["dbh"])
395
+ height = height_func(cohort_age, height_params, sp["initial_values"]["height"])
396
+ biomass_per_tree = calculate_biomass(dbh, height, sp["name"], sp["allometry"])
397
+ total_surviving += surviving
398
+ total_dbh += dbh * surviving
399
+ total_height += height * surviving
400
+ total_biomass_per_tree += biomass_per_tree * surviving
401
+ total_biomass += biomass_per_tree * surviving
402
+ n_cohorts += surviving
403
+ if total_surviving > 0:
404
+ avg_dbh = total_dbh / total_surviving
405
+ avg_height = total_height / total_surviving
406
+ avg_biomass_per_tree = total_biomass_per_tree / total_surviving
407
+ else:
408
+ avg_dbh = 0
409
+ avg_height = 0
410
+ avg_biomass_per_tree = 0
411
+ data[(sp_disp, "Surviving Trees")].append(total_surviving)
412
+ data[(sp_disp, "DBH (cm)")].append(avg_dbh)
413
+ data[(sp_disp, "Height (m)")].append(avg_height)
414
+ data[(sp_disp, "Biomass per Tree (kg)")].append(avg_biomass_per_tree)
415
+ data[(sp_disp, "Total Biomass (kg)")].append(total_biomass)
416
+ columns = pd.MultiIndex.from_tuples(list(data.keys()))
417
+ df = pd.DataFrame({col: data[col] for col in columns}, index=range(1, config["project"]["duration_years"] + 1))
418
+ df.index.name = "Year"
419
+ # Format numbers
420
+ for col in df.columns:
421
+ if "Surviving Trees" in col:
422
+ df[col] = df[col].apply(lambda x: f"{x:,.0f}")
423
+ elif "Biomass" in col or "DBH" in col or "Height" in col:
424
+ df[col] = df[col].apply(lambda x: f"{x:,.2f}")
425
+ return df
426
+
427
+ def hex_to_rgba(hex_color, alpha):
428
+ hex_color = hex_color.lstrip('#')
429
+ r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
430
+ return f'rgba({r},{g},{b},{alpha})'
431
+
432
  with gr.Blocks() as demo:
433
  gr.Markdown("# Mangrove ER Model Dashboard\nDeclining Increment Model Only")
434
  with gr.Tabs():
 
450
  carbon_plot = gr.Plot()
451
  biomass_plot = gr.Plot()
452
  annual_plot = gr.Plot()
453
+ # Add new growth/increment curves plot
454
+ growth_plot = gr.Plot(label="Growth & Increment Curves (DBH/Height)")
455
  summary_box = gr.Textbox(label="Summary", lines=12)
456
  results_box = gr.Dataframe(label="Project Results (Annual)")
457
  species_box = gr.Dataframe(label="Species Results (Annual)")
458
  survival_box = gr.Dataframe(label="Surviving Trees Table")
459
+ # Replace size_table with biomass debug table
460
+ biomass_debug_table = gr.Dataframe(label="Biomass Table (inputs & outputs per year)")
461
 
462
  def update_declining_increment(y1, y2, y3, y4, y5):
463
  config = update_planting_schedule(MODEL_CONFIGS["Declining Increment"], [y1, y2, y3, y4, y5])
 
471
  plots = create_all_plots(results, species_results, config)
472
  summary = create_summary(results, species_results, config, model)
473
  survival_table = create_survival_table(model)
474
+ growth_fig = create_growth_increment_plots(config, model_type="declining_increment")
475
+ biomass_debug_df = create_biomass_debug_table(config)
476
  results_fmt = results.copy()
477
  species_results_fmt = species_results.copy()
478
  for col in results_fmt.columns:
 
481
  for col in species_results_fmt.columns:
482
  if species_results_fmt[col].dtype in [float, int]:
483
  species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
484
+ return plots[0], plots[2], plots[1], growth_fig, summary, results_fmt, species_results_fmt, survival_table, biomass_debug_df
485
 
486
  update_btn.click(
487
  update_declining_increment,
488
  inputs=[year_1, year_2, year_3, year_4, year_5],
489
+ outputs=[carbon_plot, biomass_plot, annual_plot, growth_plot, summary_box, results_box, species_box, survival_box, biomass_debug_table]
490
  )
491
 
492
  # Show initial results
493
+ c, b, a, g, summary, r, s, surv, biomass_debug_df = update_declining_increment(2500, 2500, 0, 0, 0)
494
  carbon_plot.value = c
495
  biomass_plot.value = b
496
  annual_plot.value = a
497
+ growth_plot.value = g
498
  summary_box.value = summary
499
  results_box.value = r
500
  species_box.value = s
501
  survival_box.value = surv
502
+ biomass_debug_table.value = biomass_debug_df
503
  demo.launch(share=True)
src/er_model.py CHANGED
@@ -48,6 +48,7 @@ class CarbonConfig:
48
  buffer_percentage: float
49
  leakage_percentage: float
50
  baseline_emissions: float
 
51
 
52
 
53
  class ERModel:
@@ -58,34 +59,32 @@ class ERModel:
58
  mortality, and carbon conversion factors.
59
  """
60
 
61
- def __init__(self, config_path: Path):
62
  """
63
- Initialize the model from a YAML configuration file.
64
-
65
  Args:
66
  config_path: Path to the YAML configuration file
 
67
  """
68
- with open(config_path) as f:
69
- config = yaml.safe_load(f)
70
-
71
- self.species = [Species(**s) for s in config["species"]]
72
- self.project = ProjectConfig(**config["project"])
73
- self.carbon = CarbonConfig(**config["carbon"])
74
-
75
- # Store scenario parameters if provided
76
- self.scenarios = config.get("scenarios", {
77
  "area": 1000.0,
78
  "dbh_range": [1.0, 20.0],
79
  "height_range": [0.5, 12.0],
80
  "growth_rate_factor": 1.0
81
  })
82
-
83
- # Initialize results storage
84
  self.results: Optional[pd.DataFrame] = None
85
  self.species_results: Optional[pd.DataFrame] = None
86
  self.scenario_results: Optional[pd.DataFrame] = None
87
-
88
- self.growth_model = config.get('growth_model', 'chapman_richards')
89
 
90
  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:
91
  """
@@ -210,35 +209,34 @@ class ERModel:
210
  # Continuous formula: initial + r0 * (age - age^2/(2*Tm))
211
  return initial_value + r0 * (age - age**2 / (2 * T_m))
212
 
213
- def calculate_carbon_for_species(self, species: Species, age: int, area: float) -> float:
214
  """
215
- Calculate carbon sequestration for a single species and age.
216
  Args:
217
  species: Species parameters
218
- age: Age of trees in years
219
  area: Planted area in hectares
 
220
  Returns:
221
  Carbon sequestration in tCO2
222
  """
223
- # Calculate surviving trees using DBH-dependent mortality
 
224
  initial_trees = species.planting_density * area
225
- plateau_density = species.planting_density * area if age >= 5 else None
226
- surviving = self.calculate_cohort_surviving_trees(1, age, initial_trees, species, plateau_density, self.growth_model)
227
- # Calculate DBH and height using the selected growth model
228
  dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
229
  height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
230
- dbh = dbh_func(age, dbh_params, species.initial_values["dbh"])
231
- height = height_func(age, height_params, species.initial_values["height"])
232
- # Calculate biomass using both DBH and height
233
  biomass = calculate_biomass(dbh, height, species.name, species.allometry)
234
- # Convert to carbon
235
  carbon = calculate_carbon(
236
  biomass * surviving,
237
  self.carbon.biomass_to_carbon,
238
  self.carbon.carbon_to_co2
239
  )
240
  return carbon
241
-
242
  def run(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
243
  """
244
  Execute the full ER calculation pipeline.
@@ -248,64 +246,49 @@ class ERModel:
248
  years = range(1, self.project.duration_years + 1)
249
  results = []
250
  species_results = []
251
-
252
  for year in years:
253
  year_results = {"year": year}
254
  species_year_results = {"Year": year}
255
  total_carbon = 0
256
  cumulative_area = self.calculate_cumulative_area(year)
257
-
258
- # Calculate for each planting cohort and species
259
  for species in self.species:
260
  species_carbon = 0
261
  for planting_year, area in self.project.planting_schedule.items():
262
  py = int(planting_year.split("_")[1])
263
- initial_trees = species.planting_density * area
264
- # Use plateau_density as the year-5 value for this cohort
265
- plateau_density = species.planting_density * area if 5 <= (year - py + 1) else None
266
- surviving = self.calculate_cohort_surviving_trees(py, year, initial_trees, species, plateau_density, self.growth_model)
267
- # Calculate DBH and height using the selected growth model
268
- dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
269
- height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
270
- dbh = dbh_func(year, dbh_params, species.initial_values["dbh"])
271
- height = height_func(year, height_params, species.initial_values["height"])
272
- # Calculate biomass using both DBH and height
273
- biomass = calculate_biomass(dbh, height, species.name, species.allometry)
274
- # Convert to carbon
275
- carbon = calculate_carbon(
276
- biomass * surviving,
277
- self.carbon.biomass_to_carbon,
278
- self.carbon.carbon_to_co2
279
- )
280
  species_carbon += carbon
281
  total_carbon += species_carbon
282
- # Track per-species carbon
283
  species_key = f"{species.name} tCO2"
284
  species_year_results[species_key] = species_carbon
285
-
286
- # Add total carbon and area metrics
287
  species_year_results["Total tCO2"] = total_carbon
288
  species_year_results["Cumulative ha"] = cumulative_area
289
  species_year_results["tCO2/ha"] = total_carbon / cumulative_area if cumulative_area > 0 else 0
290
-
291
- # Apply adjustments for main results
292
  gross_carbon = total_carbon
293
  net_carbon = gross_carbon * (1 - self.carbon.buffer_percentage / 100)
294
  net_carbon -= self.carbon.leakage_percentage / 100 * gross_carbon
295
  net_carbon -= self.carbon.baseline_emissions * cumulative_area
296
-
 
 
 
 
 
297
  year_results.update({
298
  "gross_carbon": gross_carbon,
299
  "net_carbon": net_carbon,
300
- "cumulative_area": cumulative_area
 
 
 
301
  })
302
-
303
  results.append(year_results)
304
  species_results.append(species_year_results)
305
-
306
  self.results = pd.DataFrame(results)
307
  self.species_results = pd.DataFrame(species_results)
308
-
309
  return self.results, self.species_results
310
 
311
  def save_results(self, output_path: Path) -> None:
@@ -330,7 +313,10 @@ class ERModel:
330
  func = ERModel.linear_plateau_growth
331
  params = species.linear_plateau[dim]
332
  elif growth_model == "declining_increment":
333
- func = ERModel.declining_increment_growth
 
 
 
334
  params = species.declining_increment[dim]
335
  else:
336
  func = ERModel.chapman_richards_growth
 
48
  buffer_percentage: float
49
  leakage_percentage: float
50
  baseline_emissions: float
51
+ soil_carbon_per_ha_per_year: float = 0.0
52
 
53
 
54
  class ERModel:
 
59
  mortality, and carbon conversion factors.
60
  """
61
 
62
+ def __init__(self, config_path: Path = None, config: dict = None):
63
  """
64
+ Initialize the model from a YAML configuration file or a config dict.
 
65
  Args:
66
  config_path: Path to the YAML configuration file
67
+ config: Config dict (optional)
68
  """
69
+ if config is not None:
70
+ cfg = config
71
+ else:
72
+ with open(config_path) as f:
73
+ cfg = yaml.safe_load(f)
74
+ self.species = [Species(**s) for s in cfg["species"]]
75
+ self.project = ProjectConfig(**cfg["project"])
76
+ self.carbon = CarbonConfig(**cfg["carbon"])
77
+ self.scenarios = cfg.get("scenarios", {
78
  "area": 1000.0,
79
  "dbh_range": [1.0, 20.0],
80
  "height_range": [0.5, 12.0],
81
  "growth_rate_factor": 1.0
82
  })
 
 
83
  self.results: Optional[pd.DataFrame] = None
84
  self.species_results: Optional[pd.DataFrame] = None
85
  self.scenario_results: Optional[pd.DataFrame] = None
86
+ self.growth_model = cfg.get('growth_model', 'chapman_richards')
87
+ self.continuous_growth = cfg.get('continuous_growth', False)
88
 
89
  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:
90
  """
 
209
  # Continuous formula: initial + r0 * (age - age^2/(2*Tm))
210
  return initial_value + r0 * (age - age**2 / (2 * T_m))
211
 
212
+ def calculate_carbon_for_species(self, species: Species, age: int, area: float, cohort_age: int) -> float:
213
  """
214
+ Calculate carbon sequestration for a single species, cohort, and cohort age.
215
  Args:
216
  species: Species parameters
217
+ age: Project year (not used for growth)
218
  area: Planted area in hectares
219
+ cohort_age: Age of this cohort (years since planting)
220
  Returns:
221
  Carbon sequestration in tCO2
222
  """
223
+ if cohort_age < 1:
224
+ return 0
225
  initial_trees = species.planting_density * area
226
+ plateau_density = species.planting_density * area if cohort_age >= 5 else None
227
+ surviving = self.calculate_cohort_surviving_trees(1, cohort_age, initial_trees, species, plateau_density, self.growth_model)
 
228
  dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
229
  height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
230
+ dbh = dbh_func(cohort_age, dbh_params, species.initial_values["dbh"])
231
+ height = height_func(cohort_age, height_params, species.initial_values["height"])
 
232
  biomass = calculate_biomass(dbh, height, species.name, species.allometry)
 
233
  carbon = calculate_carbon(
234
  biomass * surviving,
235
  self.carbon.biomass_to_carbon,
236
  self.carbon.carbon_to_co2
237
  )
238
  return carbon
239
+
240
  def run(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
241
  """
242
  Execute the full ER calculation pipeline.
 
246
  years = range(1, self.project.duration_years + 1)
247
  results = []
248
  species_results = []
 
249
  for year in years:
250
  year_results = {"year": year}
251
  species_year_results = {"Year": year}
252
  total_carbon = 0
253
  cumulative_area = self.calculate_cumulative_area(year)
 
 
254
  for species in self.species:
255
  species_carbon = 0
256
  for planting_year, area in self.project.planting_schedule.items():
257
  py = int(planting_year.split("_")[1])
258
+ cohort_age = year - py + 1
259
+ if cohort_age < 1:
260
+ continue
261
+ # Use correct cohort age for all calculations
262
+ carbon = self.calculate_carbon_for_species(species, year, area, cohort_age)
 
 
 
 
 
 
 
 
 
 
 
 
263
  species_carbon += carbon
264
  total_carbon += species_carbon
 
265
  species_key = f"{species.name} tCO2"
266
  species_year_results[species_key] = species_carbon
 
 
267
  species_year_results["Total tCO2"] = total_carbon
268
  species_year_results["Cumulative ha"] = cumulative_area
269
  species_year_results["tCO2/ha"] = total_carbon / cumulative_area if cumulative_area > 0 else 0
 
 
270
  gross_carbon = total_carbon
271
  net_carbon = gross_carbon * (1 - self.carbon.buffer_percentage / 100)
272
  net_carbon -= self.carbon.leakage_percentage / 100 * gross_carbon
273
  net_carbon -= self.carbon.baseline_emissions * cumulative_area
274
+ # Add soil carbon if present
275
+ soil_carbon = 0
276
+ if hasattr(self.carbon, 'soil_carbon_per_ha_per_year'):
277
+ soil_carbon = self.carbon.soil_carbon_per_ha_per_year * cumulative_area
278
+ gross_carbon_with_soil = gross_carbon + soil_carbon
279
+ net_carbon_with_soil = net_carbon + soil_carbon
280
  year_results.update({
281
  "gross_carbon": gross_carbon,
282
  "net_carbon": net_carbon,
283
+ "cumulative_area": cumulative_area,
284
+ "gross_carbon_with_soil": gross_carbon_with_soil,
285
+ "net_carbon_with_soil": net_carbon_with_soil,
286
+ "soil_carbon": soil_carbon
287
  })
 
288
  results.append(year_results)
289
  species_results.append(species_year_results)
 
290
  self.results = pd.DataFrame(results)
291
  self.species_results = pd.DataFrame(species_results)
 
292
  return self.results, self.species_results
293
 
294
  def save_results(self, output_path: Path) -> None:
 
313
  func = ERModel.linear_plateau_growth
314
  params = species.linear_plateau[dim]
315
  elif growth_model == "declining_increment":
316
+ if getattr(self, 'continuous_growth', False):
317
+ func = ERModel.continuous_declining_increment_growth
318
+ else:
319
+ func = ERModel.declining_increment_growth
320
  params = species.declining_increment[dim]
321
  else:
322
  func = ERModel.chapman_richards_growth
tests/test_er_model.py CHANGED
@@ -166,4 +166,20 @@ def test_dbh_dependent_mortality():
166
  N_live = N_live * (1 - m)
167
  if y == 5:
168
  N_live = plateau_density
169
- assert N_live == plateau_density
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  N_live = N_live * (1 - m)
167
  if y == 5:
168
  N_live = plateau_density
169
+ assert N_live == plateau_density
170
+
171
+
172
+ def test_allometric_equation_species_A_B():
173
+ """Test Zanvo et al. 2023 allometric equations for both species."""
174
+ from src.allometry import calculate_biomass
175
+ # Test values
176
+ dbh = 10.0 # cm
177
+ height = 5.0 # m
178
+ # species_A (Rhizophora spp.)
179
+ expected_A = 1.938 * (dbh**2 * height)**0.67628
180
+ result_A = calculate_biomass(dbh, height, "species_A", {})
181
+ assert np.isclose(result_A, expected_A, rtol=1e-6), f"species_A: got {result_A}, expected {expected_A}"
182
+ # species_B (Avicennia germinans)
183
+ expected_B = 1.486 * (dbh**2 * height)**0.55864
184
+ result_B = calculate_biomass(dbh, height, "species_B", {})
185
+ assert np.isclose(result_B, expected_B, rtol=1e-6), f"species_B: got {result_B}, expected {expected_B}"