malcolmSQ commited on
Commit
ce27302
·
1 Parent(s): 280f88c

Finalize modularization, clean up UI, prep for new features

Browse files
dashboard/app.py CHANGED
@@ -8,9 +8,6 @@ Mangrove ER Model Dashboard
8
  import gradio as gr
9
  from pathlib import Path
10
  from src.er_model import ERModel
11
- import yaml
12
- import tempfile
13
- import matplotlib.pyplot as plt
14
  import pandas as pd
15
  import numpy as np
16
  import plotly.graph_objs as go
@@ -18,12 +15,12 @@ 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", # ARCHIVED/NOT IN USE
25
- # "Simple Linear": "configs/linear.yaml", # ARCHIVED/NOT IN USE
26
- # "Linear Plateau": "configs/linear_plateau.yaml", # ARCHIVED/NOT IN USE
27
  "Declining Increment": "configs/declining_increment.yaml"
28
  }
29
 
@@ -98,115 +95,6 @@ def run_model(config_path):
98
  species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
99
  return (*plots, summary, results_fmt, species_results_fmt, survival_table)
100
 
101
- def check_complex(arr, label):
102
- if np.iscomplexobj(arr):
103
- complex_indices = np.where(np.iscomplex(arr))[0]
104
- warnings.warn(f"[ERROR] Complex values in {label}: indices={complex_indices}, values={arr[complex_indices]}")
105
- traceback.print_stack()
106
-
107
- def create_growth_increment_plots(config, model_type=None):
108
- """
109
- Create a 2x2 grid of growth and increment plots for DBH and Height using Plotly.
110
- model_type: 'chapman_richards', 'linear', 'linear_plateau', or 'declining_increment'
111
- """
112
- if model_type is None:
113
- model_type = config.get('growth_model', 'chapman_richards')
114
- N = config["project"]["duration_years"]
115
- ages = np.arange(0, N + 1) # 0 to N
116
- ages_inc = np.arange(1, N + 1) # 1 to N
117
- fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
118
- from src.er_model import ERModel
119
- use_continuous = config.get('continuous_growth', False)
120
- for sp in config["species"]:
121
- name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
122
- initial_dbh = sp["initial_values"]["dbh"]
123
- initial_height = sp["initial_values"]["height"]
124
- if model_type == "linear":
125
- dbh = [ERModel.linear_growth(t, sp["linear"]["dbh"], initial_dbh) for t in ages]
126
- height = [ERModel.linear_growth(t, sp["linear"]["height"], initial_height) for t in ages]
127
- elif model_type == "linear_plateau":
128
- dbh = [ERModel.linear_plateau_growth(t, sp["linear_plateau"]["dbh"], initial_dbh) for t in ages]
129
- height = [ERModel.linear_plateau_growth(t, sp["linear_plateau"]["height"], initial_height) for t in ages]
130
- elif model_type == "declining_increment":
131
- if use_continuous:
132
- dbh = [ERModel.continuous_declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
133
- height = [ERModel.continuous_declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
134
- else:
135
- dbh = [ERModel.declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
136
- height = [ERModel.declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
137
- else:
138
- dbh = [ERModel.chapman_richards_growth(t, sp["chapman_richards"]["dbh"], initial_dbh) for t in ages]
139
- height = [ERModel.chapman_richards_growth(t, sp["chapman_richards"]["height"], initial_height) for t in ages]
140
- dbh = np.array(dbh)
141
- height = np.array(height)
142
- check_complex(dbh, f"{name} DBH (growth)")
143
- check_complex(height, f"{name} Height (growth)")
144
- dbh_inc = dbh[1:] - dbh[:-1]
145
- height_inc = height[1:] - height[:-1]
146
- check_complex(dbh_inc, f"{name} DBH Δ (increment)")
147
- check_complex(height_inc, f"{name} Height Δ (increment)")
148
- 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)
149
- 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)
150
- 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)
151
- 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)
152
- fig.update_layout(height=700, width=900, title_text="Growth and Increment Curves", hovermode="x unified")
153
- fig.update_xaxes(title_text="Age (years)", row=1, col=1)
154
- fig.update_xaxes(title_text="Age (years)", row=1, col=2)
155
- fig.update_xaxes(title_text="Age (years)", row=2, col=1)
156
- fig.update_xaxes(title_text="Age (years)", row=2, col=2)
157
- fig.update_yaxes(title_text="Size (cm)", row=1, col=1)
158
- fig.update_yaxes(title_text="Size (m)", row=1, col=2)
159
- fig.update_yaxes(title_text="Annual increment (cm/year)", row=2, col=1)
160
- fig.update_yaxes(title_text="Annual increment (m/year)", row=2, col=2)
161
- return fig
162
-
163
- def create_all_plots(results, species_results, config):
164
- # Diagnostic: Check for complex numbers in results DataFrame columns used for plotting
165
- for col in ["gross_carbon", "net_carbon"]:
166
- arr = np.array(results[col])
167
- check_complex(arr, f"results['{col}']")
168
- # 1. Carbon curve (gross and net, with and without soil)
169
- fig1 = go.Figure()
170
- fig1.add_trace(go.Scatter(x=results["year"], y=results["gross_carbon"], mode="lines+markers", name="Gross Carbon", line=dict(width=2, color="blue")))
171
- fig1.add_trace(go.Scatter(x=results["year"], y=results["net_carbon"], mode="lines+markers", name="Net Carbon", line=dict(width=2, color="red")))
172
- if "gross_carbon_with_soil" in results.columns:
173
- # For backward compatibility, but we now sum soil carbon in summary, so recalc here for plot
174
- soil_cumsum = results["soil_carbon"].cumsum()
175
- gross_with_soil = results["gross_carbon"] + soil_cumsum
176
- net_with_soil = results["net_carbon"] + soil_cumsum
177
- 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")))
178
- 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")))
179
- fig1.update_layout(title="Carbon Sequestration Over Time", xaxis_title="Year", yaxis_title="Carbon (tCO2)", hovermode="x unified", template="plotly_white")
180
-
181
- # 2. Annual carbon (ERs)
182
- annual_ers = results["net_carbon"].diff().fillna(results["net_carbon"].iloc[0])
183
- arr = np.array(annual_ers)
184
- check_complex(arr, "annual_ers")
185
- fig2 = go.Figure()
186
- fig2.add_trace(go.Bar(x=results["year"], y=annual_ers, name="Annual ERs", marker_color="#2ecc71", opacity=0.7))
187
- fig2.update_layout(title="Annual Emission Reductions", xaxis_title="Year", yaxis_title="Annual ERs (tCO2)", hovermode="x unified", template="plotly_white")
188
-
189
- # 3. Biomass per tree for all species (refactored to use model.species_metrics)
190
- years = results["year"]
191
- fig3 = go.Figure()
192
- import tempfile, yaml
193
- from src.er_model import ERModel
194
- with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
195
- yaml.dump(config, tmp)
196
- tmp_path = tmp.name
197
- model = ERModel(Path(tmp_path))
198
- model.run()
199
- df = model.species_metrics.copy()
200
- for sp in config["species"]:
201
- name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
202
- # Get per-tree biomass for this species
203
- sp_biomass = df[df["Species"] == sp["name"]].set_index("Year")["Biomass per Tree (kg)"]
204
- # Ensure correct order and fill missing years with NaN
205
- sp_biomass = sp_biomass.reindex(years)
206
- fig3.add_trace(go.Scatter(x=years, y=sp_biomass, mode="lines+markers", name=name, line=dict(width=2)))
207
- fig3.update_layout(title="Total Biomass per Tree", xaxis_title="Year since planting", yaxis_title="Biomass (kg per tree)", hovermode="x unified", template="plotly_white")
208
- return (fig1, fig2, fig3)
209
-
210
  def create_summary(results, species_results, config, model):
211
  total_area = sum(config["project"]["planting_schedule"].values())
212
  final_gross = results["gross_carbon"].iloc[-1]
@@ -264,85 +152,6 @@ def create_summary(results, species_results, config, model):
264
  )
265
  return summary
266
 
267
- def create_size_table(config):
268
- """
269
- Returns a DataFrame with ages as rows and columns for each species and dimension (dbh, height).
270
- """
271
- from src.er_model import ERModel
272
- ages = range(0, config["project"]["duration_years"] + 1)
273
- columns = []
274
- data = {}
275
- for sp in config["species"]:
276
- sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
277
- for dim in ["dbh", "height"]:
278
- col = (sp_disp, dim)
279
- columns.append(col)
280
- data[col] = []
281
- for age in ages:
282
- for sp in config["species"]:
283
- sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
284
- initial_dbh = sp["initial_values"]["dbh"]
285
- initial_height = sp["initial_values"]["height"]
286
- model_type = config.get('growth_model', 'chapman_richards')
287
- use_continuous = config.get('continuous_growth', False)
288
- if model_type == "linear":
289
- dbh = ERModel.linear_growth(age, sp["linear"]["dbh"], initial_dbh)
290
- height = ERModel.linear_growth(age, sp["linear"]["height"], initial_height)
291
- elif model_type == "linear_plateau":
292
- dbh = ERModel.linear_plateau_growth(age, sp["linear_plateau"]["dbh"], initial_dbh)
293
- height = ERModel.linear_plateau_growth(age, sp["linear_plateau"]["height"], initial_height)
294
- elif model_type == "declining_increment":
295
- if use_continuous:
296
- dbh = ERModel.continuous_declining_increment_growth(age, sp["declining_increment"]["dbh"], initial_dbh)
297
- height = ERModel.continuous_declining_increment_growth(age, sp["declining_increment"]["height"], initial_height)
298
- else:
299
- dbh = ERModel.declining_increment_growth(age, sp["declining_increment"]["dbh"], initial_dbh)
300
- height = ERModel.declining_increment_growth(age, sp["declining_increment"]["height"], initial_height)
301
- else:
302
- dbh = ERModel.chapman_richards_growth(age, sp["chapman_richards"]["dbh"], initial_dbh)
303
- height = ERModel.chapman_richards_growth(age, sp["chapman_richards"]["height"], initial_height)
304
- data[(sp_disp, "dbh")].append(round(dbh, 5))
305
- data[(sp_disp, "height")].append(round(height, 5))
306
- # Build MultiIndex columns
307
- columns = pd.MultiIndex.from_tuples(columns, names=["Species", "Dimension"])
308
- df = pd.DataFrame(data, index=ages)
309
- df.index.name = "Age"
310
- df = df[columns] # ensure order
311
- return df
312
-
313
- def pretty_table_title(title):
314
- return f"<span style='font-size:1.3em; font-weight:bold'>{title}</span>"
315
-
316
- def create_biomass_debug_table(config):
317
- """
318
- Returns a DataFrame with years as rows and columns for each species:
319
- - Surviving Trees
320
- - DBH (cm)
321
- - Height (m)
322
- - Biomass per Tree (kg)
323
- - Total Biomass (kg)
324
- Uses model.species_metrics for all values.
325
- """
326
- from src.er_model import ERModel
327
- import tempfile, yaml
328
- with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
329
- yaml.dump(config, tmp)
330
- tmp_path = tmp.name
331
- model = ERModel(Path(tmp_path))
332
- model.run()
333
- df = model.species_metrics.copy()
334
- # Pivot to multi-index columns: (Species, Metric)
335
- df = df.pivot(index="Year", columns="Species")
336
- # Flatten columns
337
- df.columns = [f"{SPECIES_DISPLAY_NAMES.get(sp, sp)}, {metric}" for metric, sp in df.columns]
338
- # Format numbers
339
- for col in df.columns:
340
- if "Surviving Trees" in col:
341
- df[col] = df[col].apply(lambda x: f"{x:,.0f}")
342
- elif "Biomass" in col or "DBH" in col or "Height" in col:
343
- df[col] = df[col].apply(lambda x: f"{x:,.2f}")
344
- return df
345
-
346
  def hex_to_rgba(hex_color, alpha):
347
  hex_color = hex_color.lstrip('#')
348
  r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
@@ -380,8 +189,6 @@ with gr.Blocks() as demo:
380
 
381
  def update_declining_increment(y1, y2, y3, y4, y5):
382
  config = update_planting_schedule(MODEL_CONFIGS["Declining Increment"], [y1, y2, y3, y4, y5])
383
- import tempfile
384
- import yaml
385
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
386
  yaml.dump(config, tmp)
387
  tmp_path = tmp.name
@@ -391,7 +198,7 @@ with gr.Blocks() as demo:
391
  summary = create_summary(results, species_results, config, model)
392
  survival_table = create_survival_table(model)
393
  growth_fig = create_growth_increment_plots(config, model_type="declining_increment")
394
- biomass_debug_df = create_biomass_debug_table(config)
395
  results_fmt = results.copy()
396
  species_results_fmt = species_results.copy()
397
  for col in results_fmt.columns:
 
8
  import gradio as gr
9
  from pathlib import Path
10
  from src.er_model import ERModel
 
 
 
11
  import pandas as pd
12
  import numpy as np
13
  import plotly.graph_objs as go
 
15
  import warnings
16
  import traceback
17
  from src.allometry import calculate_biomass
18
+ from dashboard.plots import check_complex, create_growth_increment_plots, create_all_plots
19
+ from dashboard.tables import create_survival_table, create_biomass_table
20
+ import yaml
21
+ import tempfile
22
 
23
  MODEL_CONFIGS = {
 
 
 
24
  "Declining Increment": "configs/declining_increment.yaml"
25
  }
26
 
 
95
  species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
96
  return (*plots, summary, results_fmt, species_results_fmt, survival_table)
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  def create_summary(results, species_results, config, model):
99
  total_area = sum(config["project"]["planting_schedule"].values())
100
  final_gross = results["gross_carbon"].iloc[-1]
 
152
  )
153
  return summary
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  def hex_to_rgba(hex_color, alpha):
156
  hex_color = hex_color.lstrip('#')
157
  r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
 
189
 
190
  def update_declining_increment(y1, y2, y3, y4, y5):
191
  config = update_planting_schedule(MODEL_CONFIGS["Declining Increment"], [y1, y2, y3, y4, y5])
 
 
192
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
193
  yaml.dump(config, tmp)
194
  tmp_path = tmp.name
 
198
  summary = create_summary(results, species_results, config, model)
199
  survival_table = create_survival_table(model)
200
  growth_fig = create_growth_increment_plots(config, model_type="declining_increment")
201
+ biomass_debug_df = create_biomass_table(config)
202
  results_fmt = results.copy()
203
  species_results_fmt = species_results.copy()
204
  for col in results_fmt.columns:
dashboard/plots/__init__.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Plotting functions for the Mangrove ER Dashboard.
3
+ """
4
+ # Imports will be updated as needed when moving functions from app.py
5
+ import plotly.graph_objs as go
6
+ from plotly.subplots import make_subplots
7
+ import numpy as np
8
+ import warnings
9
+ import traceback
10
+ from src.allometry import calculate_biomass
11
+ from src.er_model import ERModel
12
+ from pathlib import Path
13
+
14
+ # Helper for checking complex numbers (copied from app.py)
15
+ def check_complex(arr, label):
16
+ if np.iscomplexobj(arr):
17
+ complex_indices = np.where(np.iscomplex(arr))[0]
18
+ warnings.warn(f"[ERROR] Complex values in {label}: indices={complex_indices}, values={arr[complex_indices]}")
19
+ traceback.print_stack()
20
+
21
+ def create_growth_increment_plots(config, model_type=None):
22
+ """
23
+ Create a 2x2 grid of growth and increment plots for DBH and Height using Plotly.
24
+ model_type: 'chapman_richards', 'linear', 'linear_plateau', or 'declining_increment'
25
+ """
26
+ if model_type is None:
27
+ model_type = config.get('growth_model', 'chapman_richards')
28
+ N = config["project"]["duration_years"]
29
+ ages = np.arange(0, N + 1) # 0 to N
30
+ ages_inc = np.arange(1, N + 1) # 1 to N
31
+ fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
32
+ from src.er_model import ERModel
33
+ SPECIES_DISPLAY_NAMES = {
34
+ 'species_A': 'Rhizophora spp.',
35
+ 'species_B': 'Avicennia germinans'
36
+ }
37
+ use_continuous = config.get('continuous_growth', False)
38
+ for sp in config["species"]:
39
+ name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
40
+ initial_dbh = sp["initial_values"]["dbh"]
41
+ initial_height = sp["initial_values"]["height"]
42
+ if model_type == "linear":
43
+ dbh = [ERModel.linear_growth(t, sp["linear"]["dbh"], initial_dbh) for t in ages]
44
+ height = [ERModel.linear_growth(t, sp["linear"]["height"], initial_height) for t in ages]
45
+ elif model_type == "linear_plateau":
46
+ dbh = [ERModel.linear_plateau_growth(t, sp["linear_plateau"]["dbh"], initial_dbh) for t in ages]
47
+ height = [ERModel.linear_plateau_growth(t, sp["linear_plateau"]["height"], initial_height) for t in ages]
48
+ elif model_type == "declining_increment":
49
+ if use_continuous:
50
+ dbh = [ERModel.continuous_declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
51
+ height = [ERModel.continuous_declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
52
+ else:
53
+ dbh = [ERModel.declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
54
+ height = [ERModel.declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
55
+ else:
56
+ dbh = [ERModel.chapman_richards_growth(t, sp["chapman_richards"]["dbh"], initial_dbh) for t in ages]
57
+ height = [ERModel.chapman_richards_growth(t, sp["chapman_richards"]["height"], initial_height) for t in ages]
58
+ dbh = np.array(dbh)
59
+ height = np.array(height)
60
+ check_complex(dbh, f"{name} DBH (growth)")
61
+ check_complex(height, f"{name} Height (growth)")
62
+ dbh_inc = dbh[1:] - dbh[:-1]
63
+ height_inc = height[1:] - height[:-1]
64
+ check_complex(dbh_inc, f"{name} DBH Δ (increment)")
65
+ check_complex(height_inc, f"{name} Height Δ (increment)")
66
+ 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)
67
+ 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)
68
+ 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)
69
+ 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)
70
+ fig.update_layout(height=700, width=900, title_text="Growth and Increment Curves", hovermode="x unified")
71
+ fig.update_xaxes(title_text="Age (years)", row=1, col=1)
72
+ fig.update_xaxes(title_text="Age (years)", row=1, col=2)
73
+ fig.update_xaxes(title_text="Age (years)", row=2, col=1)
74
+ fig.update_xaxes(title_text="Age (years)", row=2, col=2)
75
+ fig.update_yaxes(title_text="Size (cm)", row=1, col=1)
76
+ fig.update_yaxes(title_text="Size (m)", row=1, col=2)
77
+ fig.update_yaxes(title_text="Annual increment (cm/year)", row=2, col=1)
78
+ fig.update_yaxes(title_text="Annual increment (m/year)", row=2, col=2)
79
+ return fig
80
+
81
+
82
+ def create_all_plots(results, species_results, config):
83
+ # Diagnostic: Check for complex numbers in results DataFrame columns used for plotting
84
+ for col in ["gross_carbon", "net_carbon"]:
85
+ arr = np.array(results[col])
86
+ check_complex(arr, f"results['{col}']")
87
+ # 1. Carbon curve (gross and net, with and without soil)
88
+ fig1 = go.Figure()
89
+ fig1.add_trace(go.Scatter(x=results["year"], y=results["gross_carbon"], mode="lines+markers", name="Gross Carbon", line=dict(width=2, color="blue")))
90
+ fig1.add_trace(go.Scatter(x=results["year"], y=results["net_carbon"], mode="lines+markers", name="Net Carbon", line=dict(width=2, color="red")))
91
+ if "gross_carbon_with_soil" in results.columns:
92
+ # For backward compatibility, but we now sum soil carbon in summary, so recalc here for plot
93
+ soil_cumsum = results["soil_carbon"].cumsum()
94
+ gross_with_soil = results["gross_carbon"] + soil_cumsum
95
+ net_with_soil = results["net_carbon"] + soil_cumsum
96
+ 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")))
97
+ 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")))
98
+ fig1.update_layout(title="Carbon Sequestration Over Time", xaxis_title="Year", yaxis_title="Carbon (tCO2)", hovermode="x unified", template="plotly_white")
99
+
100
+ # 2. Annual carbon (ERs)
101
+ annual_ers = results["net_carbon"].diff().fillna(results["net_carbon"].iloc[0])
102
+ arr = np.array(annual_ers)
103
+ check_complex(arr, "annual_ers")
104
+ fig2 = go.Figure()
105
+ fig2.add_trace(go.Bar(x=results["year"], y=annual_ers, name="Annual ERs", marker_color="#2ecc71", opacity=0.7))
106
+ fig2.update_layout(title="Annual Emission Reductions", xaxis_title="Year", yaxis_title="Annual ERs (tCO2)", hovermode="x unified", template="plotly_white")
107
+
108
+ # 3. Biomass per tree for all species (refactored to use model.species_metrics)
109
+ years = results["year"]
110
+ fig3 = go.Figure()
111
+ import tempfile, yaml
112
+ from src.er_model import ERModel
113
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
114
+ yaml.dump(config, tmp)
115
+ tmp_path = tmp.name
116
+ model = ERModel(Path(tmp_path))
117
+ model.run()
118
+ df = model.species_metrics.copy()
119
+ SPECIES_DISPLAY_NAMES = {
120
+ 'species_A': 'Rhizophora spp.',
121
+ 'species_B': 'Avicennia germinans'
122
+ }
123
+ for sp in config["species"]:
124
+ name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
125
+ # Get per-tree biomass for this species
126
+ sp_biomass = df[df["Species"] == sp["name"]].set_index("Year")["Biomass per Tree (kg)"]
127
+ # Ensure correct order and fill missing years with NaN
128
+ sp_biomass = sp_biomass.reindex(years)
129
+ fig3.add_trace(go.Scatter(x=years, y=sp_biomass, mode="lines+markers", name=name, line=dict(width=2)))
130
+ fig3.update_layout(title="Total Biomass per Tree", xaxis_title="Year since planting", yaxis_title="Biomass (kg per tree)", hovermode="x unified", template="plotly_white")
131
+ return (fig1, fig2, fig3)
dashboard/tables/__init__.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Table formatting functions for the Mangrove ER Dashboard.
3
+ """
4
+ # Imports will be updated as needed when moving functions from app.py
5
+ import pandas as pd
6
+ from src.er_model import ERModel
7
+ from pathlib import Path
8
+ import yaml
9
+
10
+ # Helper for pretty table titles
11
+ def pretty_table_title(title):
12
+ return f"<span style='font-size:1.3em; font-weight:bold'>{title}</span>"
13
+
14
+ # Table: Biomass Table (was debug table)
15
+ def create_biomass_table(config):
16
+ """
17
+ Returns a DataFrame with years as rows and columns for each species:
18
+ - Surviving Trees
19
+ - DBH (cm)
20
+ - Height (m)
21
+ - Biomass per Tree (kg)
22
+ - Total Biomass (kg)
23
+ Uses model.species_metrics for all values.
24
+ """
25
+ import tempfile
26
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
27
+ yaml.dump(config, tmp)
28
+ tmp_path = tmp.name
29
+ model = ERModel(Path(tmp_path))
30
+ model.run()
31
+ df = model.species_metrics.copy()
32
+ # Pivot to multi-index columns: (Species, Metric)
33
+ df = df.pivot(index="Year", columns="Species")
34
+ # Flatten columns
35
+ SPECIES_DISPLAY_NAMES = {
36
+ 'species_A': 'Rhizophora spp.',
37
+ 'species_B': 'Avicennia germinans'
38
+ }
39
+ df.columns = [f"{SPECIES_DISPLAY_NAMES.get(sp, sp)}, {metric}" for metric, sp in df.columns]
40
+ # Format numbers
41
+ for col in df.columns:
42
+ if "Surviving Trees" in col:
43
+ df[col] = df[col].apply(lambda x: f"{x:,.0f}")
44
+ elif "Biomass" in col or "DBH" in col or "Height" in col:
45
+ df[col] = df[col].apply(lambda x: f"{x:,.2f}")
46
+ return df
47
+
48
+ # Table: Surviving Trees Table
49
+ def create_survival_table(model):
50
+ """
51
+ Returns a DataFrame with years as rows and columns for each species and total surviving trees.
52
+ Uses model.species_metrics for all values.
53
+ """
54
+ SPECIES_DISPLAY_NAMES = {
55
+ 'species_A': 'Rhizophora spp.',
56
+ 'species_B': 'Avicennia germinans'
57
+ }
58
+ df = model.species_metrics.copy()
59
+ # Pivot to wide format: years as rows, species as columns
60
+ surv = df.pivot(index="Year", columns="Species", values="Surviving Trees")
61
+ # Rename columns to display names
62
+ surv.columns = [SPECIES_DISPLAY_NAMES.get(sp, sp) for sp in surv.columns]
63
+ # Add total surviving trees column
64
+ surv["Total Surviving Trees"] = surv.sum(axis=1)
65
+ # Format numbers
66
+ for col in surv.columns:
67
+ surv[col] = surv[col].apply(lambda x: f"{x:,.0f}")
68
+ surv = surv.reset_index()
69
+ return surv