er-model / dashboard /plots /__init__.py
malcolmSQ
Fix: Apply buffer multiplier to soil carbon in all calculations, plots, and summaries
9be5734
"""
Plotting functions for the Mangrove ER Dashboard.
"""
# Imports will be updated as needed when moving functions from app.py
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import warnings
import traceback
from er_model_core.allometry import calculate_biomass
from er_model_core.er_model import ERModel
from pathlib import Path
from er_model_core.growth_models.chapman_richards import chapman_richards_growth
from er_model_core.growth_models.linear import linear_growth, linear_plateau_growth
from er_model_core.growth_models.declining_increment import declining_increment_growth, continuous_declining_increment_growth
# Helper for checking complex numbers (copied from app.py)
def check_complex(arr, label):
if np.iscomplexobj(arr):
complex_indices = np.where(np.iscomplex(arr))[0]
warnings.warn(f"[ERROR] Complex values in {label}: indices={complex_indices}, values={arr[complex_indices]}")
traceback.print_stack()
def create_growth_increment_plots(config, model_type=None):
"""
Create a 2x2 grid of growth and increment plots for DBH and Height using Plotly.
model_type: 'chapman_richards', 'linear', 'linear_plateau', or 'declining_increment'
"""
if model_type is None:
model_type = config.get('growth_model', 'Unknown Model')
N = config["project"]["duration_years"]
ages = np.arange(0, N + 1) # 0 to N
ages_inc = np.arange(1, N + 1) # 1 to N
fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
from er_model_core.er_model import ERModel
SPECIES_DISPLAY_NAMES = {
'species_A': 'Rhizophora spp.',
'species_B': 'Avicennia germinans'
}
use_continuous = config.get('continuous_growth', False)
for sp in config["species"]:
name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
initial_dbh = sp["initial_values"]["dbh"]
initial_height = sp["initial_values"]["height"]
if model_type == "linear":
dbh = [linear_growth(t, sp["linear"]["dbh"], initial_dbh) for t in ages]
height = [linear_growth(t, sp["linear"]["height"], initial_height) for t in ages]
elif model_type == "linear_plateau":
dbh = [linear_plateau_growth(t, sp["linear_plateau"]["dbh"], initial_dbh) for t in ages]
height = [linear_plateau_growth(t, sp["linear_plateau"]["height"], initial_height) for t in ages]
elif model_type == "declining_increment":
if use_continuous:
dbh = [continuous_declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
height = [continuous_declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
else:
dbh = [declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
height = [declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
else:
dbh = [chapman_richards_growth(t, sp["chapman_richards"]["dbh"], initial_dbh) for t in ages]
height = [chapman_richards_growth(t, sp["chapman_richards"]["height"], initial_height) for t in ages]
dbh = np.array(dbh)
height = np.array(height)
check_complex(dbh, f"{name} DBH (growth)")
check_complex(height, f"{name} Height (growth)")
dbh_inc = dbh[1:] - dbh[:-1]
height_inc = height[1:] - height[:-1]
check_complex(dbh_inc, f"{name} DBH Δ (increment)")
check_complex(height_inc, f"{name} Height Δ (increment)")
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)
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)
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)
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)
fig.update_layout(height=700, width=900, title_text="Growth and Increment Curves", hovermode="x unified")
fig.update_xaxes(title_text="Age (years)", row=1, col=1)
fig.update_xaxes(title_text="Age (years)", row=1, col=2)
fig.update_xaxes(title_text="Age (years)", row=2, col=1)
fig.update_xaxes(title_text="Age (years)", row=2, col=2)
fig.update_yaxes(title_text="Size (cm)", row=1, col=1)
fig.update_yaxes(title_text="Size (m)", row=1, col=2)
fig.update_yaxes(title_text="Annual increment (cm/year)", row=2, col=1)
fig.update_yaxes(title_text="Annual increment (m/year)", row=2, col=2)
return fig
def create_all_plots(results, species_results, config):
# Diagnostic: Check for complex numbers in results DataFrame columns used for plotting
for col in ["gross_carbon", "buffer_carbon"]:
arr = np.array(results[col])
check_complex(arr, f"results['{col}']")
# 1. Carbon curve (gross and buffer, with and without soil)
fig1 = go.Figure()
fig1.add_trace(go.Scatter(x=results["year"], y=results["gross_carbon"], mode="lines+markers", name="Gross Carbon", line=dict(width=2, color="blue")))
fig1.add_trace(go.Scatter(x=results["year"], y=results["buffer_carbon"], mode="lines+markers", name="Buffer Carbon", line=dict(width=2, color="red")))
if "gross_carbon_with_soil" in results.columns:
# For backward compatibility, but we now sum soil carbon in summary, so recalc here for plot
soil_cumsum = results["soil_carbon"].cumsum()
gross_with_soil = results["gross_carbon"] + soil_cumsum
buffer_with_soil = results["buffer_carbon"] + (soil_cumsum * (1 - config["carbon"]["buffer_percentage"] / 100))
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")))
fig1.add_trace(go.Scatter(x=results["year"], y=buffer_with_soil, mode="lines+markers", name="Buffer Carbon (with Soil)", line=dict(width=2, dash="dot", color="#43c6ac")))
fig1.update_layout(title="Carbon Sequestration Over Time", xaxis_title="Year", yaxis_title="Carbon (tCO2)", hovermode="x unified", template="plotly_white")
# 2. Annual buffer carbon (ERs)
annual_ers = results["buffer_carbon"].diff().fillna(results["buffer_carbon"].iloc[0])
arr = np.array(annual_ers)
check_complex(arr, "annual_ers")
fig2 = go.Figure()
fig2.add_trace(go.Bar(x=results["year"], y=annual_ers, name="Annual Buffer ERs", marker_color="#2ecc71", opacity=0.7))
fig2.update_layout(title="Annual Emission Reductions (Post Buffer)", xaxis_title="Year", yaxis_title="Annual Buffer ERs (tCO2)", hovermode="x unified", template="plotly_white")
# 3. Biomass per tree for all species (refactored to use model.species_metrics)
years = results["year"]
fig3 = go.Figure()
import tempfile, yaml
from er_model_core.er_model import ERModel
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
yaml.dump(config, tmp)
tmp_path = tmp.name
model = ERModel(Path(tmp_path))
model.run()
df = model.species_metrics.copy()
SPECIES_DISPLAY_NAMES = {
'species_A': 'Rhizophora spp.',
'species_B': 'Avicennia germinans'
}
for sp in config["species"]:
name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
# Get per-tree biomass for this species
sp_biomass = df[df["Species"] == sp["name"]].set_index("Year")["Biomass per Tree (kg)"]
# Ensure correct order and fill missing years with NaN
sp_biomass = sp_biomass.reindex(years)
fig3.add_trace(go.Scatter(x=years, y=sp_biomass, mode="lines+markers", name=name, line=dict(width=2)))
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")
return (fig1, fig2, fig3)