malcolmSQ commited on
Commit ·
ce27302
1
Parent(s): 280f88c
Finalize modularization, clean up UI, prep for new features
Browse files- dashboard/app.py +5 -198
- dashboard/plots/__init__.py +131 -0
- dashboard/tables/__init__.py +69 -0
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
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|