malcolmSQ commited on
Commit
22579eb
·
1 Parent(s): fe152d4

Switch dashboard to Gradio and update dependencies

Browse files
Files changed (3) hide show
  1. configs/params.yaml +18 -8
  2. dashboard/app.py +80 -40
  3. 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
- subsequent: 1.5 # %
 
 
 
 
 
11
  chapman_richards:
12
  dbh:
13
  a: 11.07 # asymptotic maximum DBH (cm)
14
- b: 0.25 # growth rate parameter (yr^-1)
15
  c: 2.1 # shape parameter
16
  height:
17
  a: 12.0 # asymptotic maximum height (m)
18
- b: 0.25 # growth rate parameter (yr^-1)
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
- subsequent: 1.5 # %
 
 
 
 
 
35
  chapman_richards:
36
  dbh:
37
  a: 17.0 # asymptotic maximum DBH (cm)
38
- b: 0.25 # growth rate parameter (yr^-1)
39
  c: 2.1 # shape parameter
40
  height:
41
  a: 8.8 # asymptotic maximum height (m)
42
- b: 0.25 # growth rate parameter (yr^-1)
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: 2000 # ha
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: 15 # risk buffer
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
- # Milestone years plot
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
- width = 0.35
79
- x = np.arange(len(milestones))
80
- ax2.bar(x - width/2, milestone_data["gross_carbon"], width, label="Gross Carbon")
81
- ax2.bar(x + width/2, milestone_data["net_carbon"], width, label="Net Carbon")
82
- ax2.set_xticks(x)
83
- ax2.set_xticklabels(milestones)
 
84
  ax2.set_xlabel("Year")
85
- ax2.set_ylabel("Carbon (tCO2)")
86
- ax2.set_title("Carbon Sequestration at Milestone Years")
87
- ax2.legend()
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 planting densities
96
  config = load_config()
97
- rhiz_density = config["species"][0]["planting_density"] # Rhizophora density
98
- avic_density = config["species"][1]["planting_density"] # Avicennia density
99
 
100
- # Extract biomass per tree data from species_results
101
- years = species_results.index
 
102
 
103
- # Get species column names (first two columns after 'Year' should be the species)
104
- species_cols = [col for col in species_results.columns if 'tCO2' in col]
105
- if len(species_cols) >= 2:
106
- rhiz_col = species_cols[0] # First species column
107
- avic_col = species_cols[1] # Second species column
108
-
109
- # Calculate biomass per tree using actual planting densities
110
- rhiz_biomass = species_results[rhiz_col].astype(float) / (results["area_year_1"].iloc[0] * rhiz_density)
111
- avic_biomass = species_results[avic_col].astype(float) / (results["area_year_1"].iloc[0] * avic_density)
112
-
113
- ax3.plot(years, rhiz_biomass, 'o-', color='#1f77b4', label='Rhizophora spp. - Total Biomass')
114
- ax3.plot(years, avic_biomass, 'o-', color='#ff7f0e', label='Avicennia germinans - Total Biomass')
115
-
116
- ax3.set_xlabel("Year since planting")
117
- ax3.set_ylabel("Biomass (tonnes per tree)")
118
- ax3.set_title("Total Biomass per Tree")
119
- ax3.grid(True)
120
- ax3.legend()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, species_table, scenario_table
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="Milestone Years")
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="Per Species Carbon Change & Cumulative Hectares",
366
- headers=["Year", "Rhizophora tCO2", "Avicennia tCO2", "Total tCO2", "Cumulative ha", "tCO2/ha"]
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 calculate_surviving_trees(self, initial_trees: float, year: int,
80
- mortality_rates: Dict[str, float]) -> float:
81
  """
82
- Calculate surviving trees for a given year based on mortality rates.
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
- for y in range(1, year + 1):
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 = self.calculate_surviving_trees(
131
- initial_trees, age, species.mortality_rates
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 = 0
228
 
229
  # Calculate for each planting cohort and species
230
- for planting_year, area in self.project.planting_schedule.items():
231
- py = int(planting_year.split("_")[1])
232
- if py > year:
233
- continue
234
-
235
- cumulative_area += area
236
- age = year - py + 1
237
-
238
- for species in self.species:
239
- carbon = self.calculate_carbon_for_species(species, age, area)
240
- total_carbon += carbon
241
-
242
- # Track per-species carbon
243
- species_key = f"{species.name} tCO2"
244
- species_year_results[species_key] = species_year_results.get(species_key, 0) + carbon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- f"area_year_{year}": area if f"year_{year}" in self.project.planting_schedule else 0
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)