malcolmSQ commited on
Commit ·
22579eb
1
Parent(s): fe152d4
Switch dashboard to Gradio and update dependencies
Browse files- configs/params.yaml +18 -8
- dashboard/app.py +80 -40
- src/er_model.py +76 -44
configs/params.yaml
CHANGED
|
@@ -7,15 +7,20 @@ species:
|
|
| 7 |
year_3: 6 # %
|
| 8 |
year_4: 5 # %
|
| 9 |
year_5: 5 # %
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
chapman_richards:
|
| 12 |
dbh:
|
| 13 |
a: 11.07 # asymptotic maximum DBH (cm)
|
| 14 |
-
b: 0.
|
| 15 |
c: 2.1 # shape parameter
|
| 16 |
height:
|
| 17 |
a: 12.0 # asymptotic maximum height (m)
|
| 18 |
-
b: 0.
|
| 19 |
c: 2.1 # shape parameter
|
| 20 |
allometry:
|
| 21 |
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
|
@@ -31,15 +36,20 @@ species:
|
|
| 31 |
year_3: 6 # %
|
| 32 |
year_4: 5 # %
|
| 33 |
year_5: 5 # %
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
chapman_richards:
|
| 36 |
dbh:
|
| 37 |
a: 17.0 # asymptotic maximum DBH (cm)
|
| 38 |
-
b: 0.
|
| 39 |
c: 2.1 # shape parameter
|
| 40 |
height:
|
| 41 |
a: 8.8 # asymptotic maximum height (m)
|
| 42 |
-
b: 0.
|
| 43 |
c: 2.1 # shape parameter
|
| 44 |
allometry:
|
| 45 |
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
|
@@ -51,7 +61,7 @@ project:
|
|
| 51 |
duration_years: 30 # matches project_years
|
| 52 |
planting_schedule:
|
| 53 |
year_1: 2500 # ha
|
| 54 |
-
year_2:
|
| 55 |
year_3: 0 # ha
|
| 56 |
year_4: 0 # ha
|
| 57 |
year_5: 0 # ha
|
|
@@ -59,7 +69,7 @@ project:
|
|
| 59 |
carbon:
|
| 60 |
biomass_to_carbon: 0.47 # IPCC default
|
| 61 |
carbon_to_co2: 3.67 # molecular weight ratio
|
| 62 |
-
buffer_percentage:
|
| 63 |
leakage_percentage: 0 # no leakage assumed
|
| 64 |
baseline_emissions: 0 # tCO2/ha/year
|
| 65 |
|
|
|
|
| 7 |
year_3: 6 # %
|
| 8 |
year_4: 5 # %
|
| 9 |
year_5: 5 # %
|
| 10 |
+
year_6: 5 # %
|
| 11 |
+
year_7: 5 # %
|
| 12 |
+
year_8: 5 # %
|
| 13 |
+
year_9: 5 # %
|
| 14 |
+
year_10: 5 # %
|
| 15 |
+
subsequent: 1.5 # % (years 11-30)
|
| 16 |
chapman_richards:
|
| 17 |
dbh:
|
| 18 |
a: 11.07 # asymptotic maximum DBH (cm)
|
| 19 |
+
b: 0.1237 # growth rate parameter (yr^-1)
|
| 20 |
c: 2.1 # shape parameter
|
| 21 |
height:
|
| 22 |
a: 12.0 # asymptotic maximum height (m)
|
| 23 |
+
b: 0.1237 # growth rate parameter (yr^-1)
|
| 24 |
c: 2.1 # shape parameter
|
| 25 |
allometry:
|
| 26 |
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
|
|
|
| 36 |
year_3: 6 # %
|
| 37 |
year_4: 5 # %
|
| 38 |
year_5: 5 # %
|
| 39 |
+
year_6: 5 # %
|
| 40 |
+
year_7: 5 # %
|
| 41 |
+
year_8: 5 # %
|
| 42 |
+
year_9: 5 # %
|
| 43 |
+
year_10: 5 # %
|
| 44 |
+
subsequent: 1.5 # % (years 11-30)
|
| 45 |
chapman_richards:
|
| 46 |
dbh:
|
| 47 |
a: 17.0 # asymptotic maximum DBH (cm)
|
| 48 |
+
b: 0.1237 # growth rate parameter (yr^-1)
|
| 49 |
c: 2.1 # shape parameter
|
| 50 |
height:
|
| 51 |
a: 8.8 # asymptotic maximum height (m)
|
| 52 |
+
b: 0.1237 # growth rate parameter (yr^-1)
|
| 53 |
c: 2.1 # shape parameter
|
| 54 |
allometry:
|
| 55 |
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
|
|
|
| 61 |
duration_years: 30 # matches project_years
|
| 62 |
planting_schedule:
|
| 63 |
year_1: 2500 # ha
|
| 64 |
+
year_2: 2500 # ha
|
| 65 |
year_3: 0 # ha
|
| 66 |
year_4: 0 # ha
|
| 67 |
year_5: 0 # ha
|
|
|
|
| 69 |
carbon:
|
| 70 |
biomass_to_carbon: 0.47 # IPCC default
|
| 71 |
carbon_to_co2: 3.67 # molecular weight ratio
|
| 72 |
+
buffer_percentage: 20 # risk buffer
|
| 73 |
leakage_percentage: 0 # no leakage assumed
|
| 74 |
baseline_emissions: 0 # tCO2/ha/year
|
| 75 |
|
dashboard/app.py
CHANGED
|
@@ -69,55 +69,82 @@ def create_plots_and_tables(
|
|
| 69 |
# Format y-axis with comma separator
|
| 70 |
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 71 |
|
| 72 |
-
#
|
| 73 |
-
milestones = [5, 10, 15, 20, 25, 30]
|
| 74 |
-
milestone_data = results[results["year"].isin(milestones)]
|
| 75 |
-
|
| 76 |
fig2 = plt.figure(figsize=(10, 6))
|
| 77 |
ax2 = fig2.add_subplot(111)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
| 84 |
ax2.set_xlabel("Year")
|
| 85 |
-
ax2.set_ylabel("
|
| 86 |
-
ax2.set_title("
|
| 87 |
-
|
| 88 |
# Format y-axis with comma separator
|
| 89 |
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
# New biomass per tree plot
|
| 92 |
fig3 = plt.figure(figsize=(10, 6))
|
| 93 |
ax3 = fig3.add_subplot(111)
|
| 94 |
|
| 95 |
-
# Load config to get
|
| 96 |
config = load_config()
|
| 97 |
-
|
| 98 |
-
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
|
|
|
|
| 102 |
|
| 103 |
-
#
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
# Calculate total area from planting schedule
|
| 123 |
total_area = sum(float(v) for k, v in results.iloc[-1].items() if k.startswith("area_year"))
|
|
@@ -155,13 +182,26 @@ Net Carbon per ha: {format_number(net_carbon_per_ha)} tCO2/ha
|
|
| 155 |
for col in species_table.select_dtypes(include=[np.number]).columns:
|
| 156 |
species_table[col] = species_table[col].apply(format_number)
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
# Format scenario comparison table with comma separators
|
| 159 |
scenario_table = scenario_results.copy()
|
| 160 |
scenario_table.index.name = "Scenario"
|
| 161 |
for col in scenario_table.select_dtypes(include=[np.number]).columns:
|
| 162 |
scenario_table[col] = scenario_table[col].apply(format_number)
|
| 163 |
|
| 164 |
-
return fig1, fig2, fig3, summary,
|
| 165 |
|
| 166 |
|
| 167 |
def update_model(
|
|
@@ -353,7 +393,7 @@ def main():
|
|
| 353 |
# Outputs
|
| 354 |
with gr.Row():
|
| 355 |
plot1 = gr.Plot(label="Carbon Sequestration Over Time")
|
| 356 |
-
plot2 = gr.Plot(label="
|
| 357 |
|
| 358 |
with gr.Row():
|
| 359 |
plot3 = gr.Plot(label="Total Biomass per Tree")
|
|
@@ -362,8 +402,8 @@ def main():
|
|
| 362 |
|
| 363 |
with gr.Row():
|
| 364 |
species_table = gr.Dataframe(
|
| 365 |
-
label="
|
| 366 |
-
headers=["Year", "
|
| 367 |
)
|
| 368 |
scenario_table = gr.Dataframe(
|
| 369 |
label="Scenario Comparison",
|
|
|
|
| 69 |
# Format y-axis with comma separator
|
| 70 |
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 71 |
|
| 72 |
+
# Annual ERs plot (replacing milestone years plot)
|
|
|
|
|
|
|
|
|
|
| 73 |
fig2 = plt.figure(figsize=(10, 6))
|
| 74 |
ax2 = fig2.add_subplot(111)
|
| 75 |
+
|
| 76 |
+
# Calculate annual ERs (year-over-year change in net carbon)
|
| 77 |
+
annual_ers = results["net_carbon"].diff()
|
| 78 |
+
annual_ers.iloc[0] = results["net_carbon"].iloc[0] # First year is total, not difference
|
| 79 |
+
|
| 80 |
+
# Plot annual ERs as bars
|
| 81 |
+
ax2.bar(results["year"], annual_ers, color='#2ecc71', alpha=0.7)
|
| 82 |
ax2.set_xlabel("Year")
|
| 83 |
+
ax2.set_ylabel("Annual ERs (tCO2)")
|
| 84 |
+
ax2.set_title("Annual Emission Reductions")
|
| 85 |
+
|
| 86 |
# Format y-axis with comma separator
|
| 87 |
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 88 |
+
|
| 89 |
+
# Add grid for better readability
|
| 90 |
+
ax2.grid(True, axis='y', linestyle='--', alpha=0.7)
|
| 91 |
+
|
| 92 |
+
# Ensure integer ticks for years
|
| 93 |
+
ax2.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
|
| 94 |
|
| 95 |
# New biomass per tree plot
|
| 96 |
fig3 = plt.figure(figsize=(10, 6))
|
| 97 |
ax3 = fig3.add_subplot(111)
|
| 98 |
|
| 99 |
+
# Load config to get growth parameters
|
| 100 |
config = load_config()
|
| 101 |
+
rhiz_params = config["species"][0]["chapman_richards"]
|
| 102 |
+
avic_params = config["species"][1]["chapman_richards"]
|
| 103 |
|
| 104 |
+
# Get initial values
|
| 105 |
+
rhiz_init = config["species"][0]["initial_values"]
|
| 106 |
+
avic_init = config["species"][1]["initial_values"]
|
| 107 |
|
| 108 |
+
# Create time series
|
| 109 |
+
years = np.arange(len(species_results))
|
| 110 |
+
|
| 111 |
+
# Calculate DBH and height for each species over time
|
| 112 |
+
def chapman_richards(t, params, initial):
|
| 113 |
+
a, b, c = params["a"], params["b"], params["c"]
|
| 114 |
+
return initial + (a - initial) * (1 - np.exp(-b * t)) ** c
|
| 115 |
+
|
| 116 |
+
# Calculate growth for Rhizophora
|
| 117 |
+
rhiz_dbh = chapman_richards(years, rhiz_params["dbh"], rhiz_init["dbh"])
|
| 118 |
+
rhiz_height = chapman_richards(years, rhiz_params["height"], rhiz_init["height"])
|
| 119 |
+
|
| 120 |
+
# Calculate growth for Avicennia
|
| 121 |
+
avic_dbh = chapman_richards(years, avic_params["dbh"], avic_init["dbh"])
|
| 122 |
+
avic_height = chapman_richards(years, avic_params["height"], avic_init["height"])
|
| 123 |
+
|
| 124 |
+
# Calculate biomass using Zanvo equations (convert kg to tonnes)
|
| 125 |
+
rhiz_biomass = 1.938 * (rhiz_dbh**2 * rhiz_height)**0.67628 / 1000 # Convert kg to tonnes
|
| 126 |
+
avic_biomass = 1.486 * (avic_dbh**2 * avic_height)**0.55864 / 1000 # Convert kg to tonnes
|
| 127 |
+
|
| 128 |
+
# Plot with proper formatting
|
| 129 |
+
ax3.plot(years, rhiz_biomass, 'o-', color='#1f77b4', label='Rhizophora spp. - Total Biomass')
|
| 130 |
+
ax3.plot(years, avic_biomass, 'o-', color='#ff7f0e', label='Avicennia germinans - Total Biomass')
|
| 131 |
+
|
| 132 |
+
ax3.set_xlabel("Year since planting")
|
| 133 |
+
ax3.set_ylabel("Biomass (tonnes per tree)")
|
| 134 |
+
ax3.set_title("Total Biomass per Tree")
|
| 135 |
+
ax3.grid(True)
|
| 136 |
+
ax3.legend()
|
| 137 |
+
|
| 138 |
+
# Set proper axis limits
|
| 139 |
+
ax3.set_xlim(0, len(years)-1)
|
| 140 |
+
ax3.set_ylim(bottom=0) # Start y-axis at 0
|
| 141 |
+
|
| 142 |
+
# Format y-axis with comma separator
|
| 143 |
+
ax3.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 144 |
+
|
| 145 |
+
# Ensure the plot is drawn
|
| 146 |
+
fig3.tight_layout()
|
| 147 |
+
plt.draw()
|
| 148 |
|
| 149 |
# Calculate total area from planting schedule
|
| 150 |
total_area = sum(float(v) for k, v in results.iloc[-1].items() if k.startswith("area_year"))
|
|
|
|
| 182 |
for col in species_table.select_dtypes(include=[np.number]).columns:
|
| 183 |
species_table[col] = species_table[col].apply(format_number)
|
| 184 |
|
| 185 |
+
# Create annual ERs table
|
| 186 |
+
annual_results = pd.DataFrame({
|
| 187 |
+
'Year': results['year'],
|
| 188 |
+
'Annual ERs (tCO2)': annual_ers,
|
| 189 |
+
'Cumulative ERs (tCO2)': results['net_carbon'],
|
| 190 |
+
'Area Planted (ha)': results[[col for col in results.columns if col.startswith('area_year_')]].sum(axis=1),
|
| 191 |
+
'Cumulative Area (ha)': results[[col for col in results.columns if col.startswith('area_year_')]].cumsum().sum(axis=1)
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
# Format annual results table with comma separators
|
| 195 |
+
for col in annual_results.select_dtypes(include=[np.number]).columns:
|
| 196 |
+
annual_results[col] = annual_results[col].apply(format_number)
|
| 197 |
+
|
| 198 |
# Format scenario comparison table with comma separators
|
| 199 |
scenario_table = scenario_results.copy()
|
| 200 |
scenario_table.index.name = "Scenario"
|
| 201 |
for col in scenario_table.select_dtypes(include=[np.number]).columns:
|
| 202 |
scenario_table[col] = scenario_table[col].apply(format_number)
|
| 203 |
|
| 204 |
+
return fig1, fig2, fig3, summary, annual_results, scenario_table
|
| 205 |
|
| 206 |
|
| 207 |
def update_model(
|
|
|
|
| 393 |
# Outputs
|
| 394 |
with gr.Row():
|
| 395 |
plot1 = gr.Plot(label="Carbon Sequestration Over Time")
|
| 396 |
+
plot2 = gr.Plot(label="Annual Emission Reductions")
|
| 397 |
|
| 398 |
with gr.Row():
|
| 399 |
plot3 = gr.Plot(label="Total Biomass per Tree")
|
|
|
|
| 402 |
|
| 403 |
with gr.Row():
|
| 404 |
species_table = gr.Dataframe(
|
| 405 |
+
label="Annual Emission Reductions",
|
| 406 |
+
headers=["Year", "Annual ERs (tCO2)", "Cumulative ERs (tCO2)", "Area Planted (ha)", "Cumulative Area (ha)"]
|
| 407 |
)
|
| 408 |
scenario_table = gr.Dataframe(
|
| 409 |
label="Scenario Comparison",
|
src/er_model.py
CHANGED
|
@@ -76,28 +76,46 @@ class ERModel:
|
|
| 76 |
self.species_results: Optional[pd.DataFrame] = None
|
| 77 |
self.scenario_results: Optional[pd.DataFrame] = None
|
| 78 |
|
| 79 |
-
def
|
| 80 |
-
mortality_rates: Dict[str, float]) -> float:
|
| 81 |
"""
|
| 82 |
-
Calculate surviving trees for a
|
| 83 |
-
|
| 84 |
-
Args:
|
| 85 |
-
initial_trees: Initial number of trees planted
|
| 86 |
-
year: Years since planting (1-based)
|
| 87 |
-
mortality_rates: Dictionary of mortality rates by year
|
| 88 |
-
|
| 89 |
-
Returns:
|
| 90 |
-
Number of surviving trees
|
| 91 |
"""
|
|
|
|
|
|
|
|
|
|
| 92 |
surviving = initial_trees
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
rate = mortality_rates.get(f"year_{y}",
|
| 96 |
-
mortality_rates["subsequent"]) / 100.0
|
| 97 |
surviving *= (1 - rate)
|
| 98 |
-
|
| 99 |
return surviving
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
def chapman_richards_growth(self, age: float, params: Dict[str, float], initial_value: float) -> float:
|
| 102 |
"""
|
| 103 |
Calculate growth using Chapman-Richards growth equation.
|
|
@@ -116,21 +134,19 @@ class ERModel:
|
|
| 116 |
def calculate_carbon_for_species(self, species: Species, age: int, area: float) -> float:
|
| 117 |
"""
|
| 118 |
Calculate carbon sequestration for a single species and age.
|
| 119 |
-
|
| 120 |
Args:
|
| 121 |
species: Species parameters
|
| 122 |
age: Age of trees in years
|
| 123 |
area: Planted area in hectares
|
| 124 |
-
|
| 125 |
Returns:
|
| 126 |
Carbon sequestration in tCO2
|
| 127 |
"""
|
| 128 |
-
# Calculate surviving trees
|
| 129 |
initial_trees = species.planting_density * area
|
| 130 |
-
surviving =
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
# Calculate DBH and height using Chapman-Richards
|
| 135 |
dbh = self.chapman_richards_growth(
|
| 136 |
age,
|
|
@@ -142,17 +158,14 @@ class ERModel:
|
|
| 142 |
species.chapman_richards["height"],
|
| 143 |
species.initial_values["height"]
|
| 144 |
)
|
| 145 |
-
|
| 146 |
# Calculate biomass using both DBH and height
|
| 147 |
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
| 148 |
-
|
| 149 |
# Convert to carbon
|
| 150 |
carbon = calculate_carbon(
|
| 151 |
biomass * surviving,
|
| 152 |
self.carbon.biomass_to_carbon,
|
| 153 |
self.carbon.carbon_to_co2
|
| 154 |
)
|
| 155 |
-
|
| 156 |
return carbon
|
| 157 |
|
| 158 |
def run_scenario(self, area: float, dbh_range: List[float], height_range: List[float],
|
|
@@ -224,24 +237,43 @@ class ERModel:
|
|
| 224 |
year_results = {"year": year}
|
| 225 |
species_year_results = {"Year": year}
|
| 226 |
total_carbon = 0
|
| 227 |
-
cumulative_area =
|
| 228 |
|
| 229 |
# Calculate for each planting cohort and species
|
| 230 |
-
for
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
# Add total carbon and area metrics
|
| 247 |
species_year_results["Total tCO2"] = total_carbon
|
|
@@ -257,7 +289,7 @@ class ERModel:
|
|
| 257 |
year_results.update({
|
| 258 |
"gross_carbon": gross_carbon,
|
| 259 |
"net_carbon": net_carbon,
|
| 260 |
-
|
| 261 |
})
|
| 262 |
|
| 263 |
results.append(year_results)
|
|
|
|
| 76 |
self.species_results: Optional[pd.DataFrame] = None
|
| 77 |
self.scenario_results: Optional[pd.DataFrame] = None
|
| 78 |
|
| 79 |
+
def calculate_cohort_surviving_trees(self, planting_year: int, current_year: int, initial_trees: float, mortality_rates: Dict[str, float]) -> float:
|
|
|
|
| 80 |
"""
|
| 81 |
+
Calculate surviving trees for a cohort planted in planting_year, in current_year.
|
| 82 |
+
Applies cumulative mortality for the cohort's age.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
"""
|
| 84 |
+
age = current_year - planting_year + 1
|
| 85 |
+
if age < 1:
|
| 86 |
+
return 0
|
| 87 |
surviving = initial_trees
|
| 88 |
+
for y in range(1, age + 1):
|
| 89 |
+
rate = mortality_rates.get(f"year_{y}", mortality_rates["subsequent"]) / 100.0
|
|
|
|
|
|
|
| 90 |
surviving *= (1 - rate)
|
|
|
|
| 91 |
return surviving
|
| 92 |
+
|
| 93 |
+
def calculate_total_surviving_trees(self, year: int) -> Dict[str, float]:
|
| 94 |
+
"""
|
| 95 |
+
Calculate total surviving trees for each species in a given year, summing across all cohorts.
|
| 96 |
+
Returns a dict: {species_name: total_surviving_trees}
|
| 97 |
+
"""
|
| 98 |
+
totals = {}
|
| 99 |
+
for species in self.species:
|
| 100 |
+
total = 0
|
| 101 |
+
for planting_year, area in self.project.planting_schedule.items():
|
| 102 |
+
py = int(planting_year.split("_")[1])
|
| 103 |
+
initial_trees = species.planting_density * area
|
| 104 |
+
total += self.calculate_cohort_surviving_trees(py, year, initial_trees, species.mortality_rates)
|
| 105 |
+
totals[species.name] = total
|
| 106 |
+
return totals
|
| 107 |
+
|
| 108 |
+
def calculate_cumulative_area(self, year: int) -> float:
|
| 109 |
+
"""
|
| 110 |
+
Calculate cumulative area planted up to and including the given year.
|
| 111 |
+
"""
|
| 112 |
+
total = 0
|
| 113 |
+
for planting_year, area in self.project.planting_schedule.items():
|
| 114 |
+
py = int(planting_year.split("_")[1])
|
| 115 |
+
if py <= year:
|
| 116 |
+
total += area
|
| 117 |
+
return total
|
| 118 |
+
|
| 119 |
def chapman_richards_growth(self, age: float, params: Dict[str, float], initial_value: float) -> float:
|
| 120 |
"""
|
| 121 |
Calculate growth using Chapman-Richards growth equation.
|
|
|
|
| 134 |
def calculate_carbon_for_species(self, species: Species, age: int, area: float) -> float:
|
| 135 |
"""
|
| 136 |
Calculate carbon sequestration for a single species and age.
|
|
|
|
| 137 |
Args:
|
| 138 |
species: Species parameters
|
| 139 |
age: Age of trees in years
|
| 140 |
area: Planted area in hectares
|
|
|
|
| 141 |
Returns:
|
| 142 |
Carbon sequestration in tCO2
|
| 143 |
"""
|
| 144 |
+
# Calculate surviving trees using cumulative mortality for the given age
|
| 145 |
initial_trees = species.planting_density * area
|
| 146 |
+
surviving = initial_trees
|
| 147 |
+
for y in range(1, age + 1):
|
| 148 |
+
rate = species.mortality_rates.get(f"year_{y}", species.mortality_rates["subsequent"]) / 100.0
|
| 149 |
+
surviving *= (1 - rate)
|
| 150 |
# Calculate DBH and height using Chapman-Richards
|
| 151 |
dbh = self.chapman_richards_growth(
|
| 152 |
age,
|
|
|
|
| 158 |
species.chapman_richards["height"],
|
| 159 |
species.initial_values["height"]
|
| 160 |
)
|
|
|
|
| 161 |
# Calculate biomass using both DBH and height
|
| 162 |
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
|
|
|
| 163 |
# Convert to carbon
|
| 164 |
carbon = calculate_carbon(
|
| 165 |
biomass * surviving,
|
| 166 |
self.carbon.biomass_to_carbon,
|
| 167 |
self.carbon.carbon_to_co2
|
| 168 |
)
|
|
|
|
| 169 |
return carbon
|
| 170 |
|
| 171 |
def run_scenario(self, area: float, dbh_range: List[float], height_range: List[float],
|
|
|
|
| 237 |
year_results = {"year": year}
|
| 238 |
species_year_results = {"Year": year}
|
| 239 |
total_carbon = 0
|
| 240 |
+
cumulative_area = self.calculate_cumulative_area(year)
|
| 241 |
|
| 242 |
# Calculate for each planting cohort and species
|
| 243 |
+
for species in self.species:
|
| 244 |
+
species_carbon = 0
|
| 245 |
+
for planting_year, area in self.project.planting_schedule.items():
|
| 246 |
+
py = int(planting_year.split("_")[1])
|
| 247 |
+
initial_trees = species.planting_density * area
|
| 248 |
+
age = year - py + 1
|
| 249 |
+
if age < 1:
|
| 250 |
+
continue
|
| 251 |
+
# Calculate surviving trees for this cohort
|
| 252 |
+
surviving = self.calculate_cohort_surviving_trees(py, year, initial_trees, species.mortality_rates)
|
| 253 |
+
# Calculate DBH and height using Chapman-Richards
|
| 254 |
+
dbh = self.chapman_richards_growth(
|
| 255 |
+
age,
|
| 256 |
+
species.chapman_richards["dbh"],
|
| 257 |
+
species.initial_values["dbh"]
|
| 258 |
+
)
|
| 259 |
+
height = self.chapman_richards_growth(
|
| 260 |
+
age,
|
| 261 |
+
species.chapman_richards["height"],
|
| 262 |
+
species.initial_values["height"]
|
| 263 |
+
)
|
| 264 |
+
# Calculate biomass using both DBH and height
|
| 265 |
+
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
| 266 |
+
# Convert to carbon
|
| 267 |
+
carbon = calculate_carbon(
|
| 268 |
+
biomass * surviving,
|
| 269 |
+
self.carbon.biomass_to_carbon,
|
| 270 |
+
self.carbon.carbon_to_co2
|
| 271 |
+
)
|
| 272 |
+
species_carbon += carbon
|
| 273 |
+
total_carbon += species_carbon
|
| 274 |
+
# Track per-species carbon
|
| 275 |
+
species_key = f"{species.name} tCO2"
|
| 276 |
+
species_year_results[species_key] = species_carbon
|
| 277 |
|
| 278 |
# Add total carbon and area metrics
|
| 279 |
species_year_results["Total tCO2"] = total_carbon
|
|
|
|
| 289 |
year_results.update({
|
| 290 |
"gross_carbon": gross_carbon,
|
| 291 |
"net_carbon": net_carbon,
|
| 292 |
+
"cumulative_area": cumulative_area
|
| 293 |
})
|
| 294 |
|
| 295 |
results.append(year_results)
|