malcolmSQ commited on
Commit
c5df41a
·
1 Parent(s): 9047195

Update README and model docs: reflect modular structure, active/archived models, and model-driven dashboard

Browse files
dashboard/app.py CHANGED
@@ -21,9 +21,9 @@ from src.allometry import calculate_biomass
21
  import copy
22
 
23
  MODEL_CONFIGS = {
24
- "Original Model": "configs/params.yaml",
25
- "Simple Linear": "configs/linear.yaml",
26
- "Linear Plateau": "configs/linear_plateau.yaml",
27
  "Declining Increment": "configs/declining_increment.yaml"
28
  }
29
 
@@ -44,29 +44,20 @@ def update_planting_schedule(config_path, year_areas):
44
  def create_survival_table(model):
45
  """
46
  Returns a DataFrame with years as rows and columns for each species and total surviving trees.
 
47
  """
48
- years = range(1, model.project.duration_years + 1)
49
- data = {"Year": []}
50
- # Use internal names for lookup, but display friendly names
51
- internal_names = [s.name for s in model.species]
52
- display_names = [SPECIES_DISPLAY_NAMES.get(s.name, s.name) for s in model.species]
53
- for name in display_names:
54
- data[name] = []
55
- data["Total Surviving Trees"] = []
56
- for year in years:
57
- data["Year"].append(year)
58
- totals = model.calculate_total_surviving_trees(year)
59
- total = 0
60
- for internal, display in zip(internal_names, display_names):
61
- val = totals.get(internal, 0)
62
- data[display].append(val)
63
- total += val
64
- data["Total Surviving Trees"].append(total)
65
- df = pd.DataFrame(data)
66
- # Format numbers: no decimals, thousands separator
67
- for name in display_names + ["Total Surviving Trees"]:
68
- df[name] = df[name].apply(lambda x: f"{x:,.0f}")
69
- return df
70
 
71
  def run_model_from_config(config):
72
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
@@ -195,53 +186,25 @@ def create_all_plots(results, species_results, config):
195
  fig2.add_trace(go.Bar(x=results["year"], y=annual_ers, name="Annual ERs", marker_color="#2ecc71", opacity=0.7))
196
  fig2.update_layout(title="Annual Emission Reductions", xaxis_title="Year", yaxis_title="Annual ERs (tCO2)", hovermode="x unified", template="plotly_white")
197
 
198
- # 3. Biomass per tree for all species
199
  years = results["year"]
200
  fig3 = go.Figure()
 
201
  from src.er_model import ERModel
202
- growth_model = config.get('growth_model', 'chapman_richards')
 
 
 
 
 
203
  for sp in config["species"]:
204
  name = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
205
- initial_dbh = sp["initial_values"]["dbh"]
206
- initial_height = sp["initial_values"]["height"]
207
- use_continuous = config.get('continuous_growth', False)
208
- if growth_model == "linear":
209
- dbh_params = sp["linear"]["dbh"]
210
- height_params = sp["linear"]["height"]
211
- dbh = [ERModel.linear_growth(t, dbh_params, initial_dbh) for t in years]
212
- height = [ERModel.linear_growth(t, height_params, initial_height) for t in years]
213
- elif growth_model == "linear_plateau":
214
- dbh_params = sp["linear_plateau"]["dbh"]
215
- height_params = sp["linear_plateau"]["height"]
216
- dbh = [ERModel.linear_plateau_growth(t, dbh_params, initial_dbh) for t in years]
217
- height = [ERModel.linear_plateau_growth(t, height_params, initial_height) for t in years]
218
- elif growth_model == "declining_increment":
219
- dbh_params = sp["declining_increment"]["dbh"]
220
- height_params = sp["declining_increment"]["height"]
221
- if use_continuous:
222
- dbh = [ERModel.continuous_declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
223
- height = [ERModel.continuous_declining_increment_growth(t, height_params, initial_height) for t in years]
224
- else:
225
- dbh = [ERModel.declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
226
- height = [ERModel.declining_increment_growth(t, height_params, initial_height) for t in years]
227
- else: # Default to Chapman-Richards
228
- dbh_params = sp["chapman_richards"]["dbh"]
229
- height_params = sp["chapman_richards"]["height"]
230
- def chapman_richards(t, params, initial):
231
- a, b, c = params["a"], params["b"], params["c"]
232
- return initial + (a - initial) * (1 - np.exp(-b * t)) ** c
233
- dbh = [chapman_richards(t, dbh_params, initial_dbh) for t in years]
234
- height = [chapman_richards(t, height_params, initial_height) for t in years]
235
- if "Zanvo" in sp["allometry"]["equation"]:
236
- if "1.938" in sp["allometry"]["equation"]:
237
- biomass = 1.938 * (np.array(dbh) ** 2 * np.array(height)) ** 0.67628 / 1000
238
- else:
239
- biomass = 1.486 * (np.array(dbh) ** 2 * np.array(height)) ** 0.55864 / 1000
240
- else:
241
- biomass = dbh
242
- check_complex(biomass, f"{name} biomass (allometry)")
243
- fig3.add_trace(go.Scatter(x=years, y=biomass, mode="lines+markers", name=name, line=dict(width=2)))
244
- fig3.update_layout(title="Total Biomass per Tree", xaxis_title="Year since planting", yaxis_title="Biomass (tonnes per tree)", hovermode="x unified", template="plotly_white")
245
  return (fig1, fig2, fig3)
246
 
247
  def create_summary(results, species_results, config, model):
@@ -356,66 +319,22 @@ def create_biomass_debug_table(config):
356
  - Surviving Trees
357
  - DBH (cm)
358
  - Height (m)
359
- - Biomass per tree (kg)
360
  - Total Biomass (kg)
361
- Uses cohort-level growth (age = year - planting_year + 1).
362
  """
363
  from src.er_model import ERModel
364
- from src.allometry import calculate_biomass
365
- years = range(1, config["project"]["duration_years"] + 1)
366
- data = {}
367
- for sp in config["species"]:
368
- sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
369
- data[(sp_disp, "Surviving Trees")] = []
370
- data[(sp_disp, "DBH (cm)")] = []
371
- data[(sp_disp, "Height (m)")] = []
372
- data[(sp_disp, "Biomass per Tree (kg)")] = []
373
- data[(sp_disp, "Total Biomass (kg)")] = []
374
- model = ERModel(config=config)
375
- for year in years:
376
- for sp in config["species"]:
377
- sp_disp = SPECIES_DISPLAY_NAMES.get(sp["name"], sp["name"])
378
- total_surviving = 0
379
- total_biomass = 0
380
- total_dbh = 0
381
- total_height = 0
382
- total_biomass_per_tree = 0
383
- n_cohorts = 0
384
- for planting_year, area in config["project"]["planting_schedule"].items():
385
- py = int(planting_year.split("_")[1])
386
- cohort_age = year - py + 1
387
- if cohort_age < 1:
388
- continue
389
- initial_trees = sp["planting_density"] * area
390
- plateau_density = sp["planting_density"] * area if cohort_age >= 5 else None
391
- surviving = model.calculate_cohort_surviving_trees(1, cohort_age, initial_trees, model.species[0], plateau_density, model.growth_model)
392
- dbh_func, dbh_params = model.get_growth_function_and_params(model.species[0], model.growth_model, 'dbh')
393
- height_func, height_params = model.get_growth_function_and_params(model.species[0], model.growth_model, 'height')
394
- dbh = dbh_func(cohort_age, dbh_params, sp["initial_values"]["dbh"])
395
- height = height_func(cohort_age, height_params, sp["initial_values"]["height"])
396
- biomass_per_tree = calculate_biomass(dbh, height, sp["name"], sp["allometry"])
397
- total_surviving += surviving
398
- total_dbh += dbh * surviving
399
- total_height += height * surviving
400
- total_biomass_per_tree += biomass_per_tree * surviving
401
- total_biomass += biomass_per_tree * surviving
402
- n_cohorts += surviving
403
- if total_surviving > 0:
404
- avg_dbh = total_dbh / total_surviving
405
- avg_height = total_height / total_surviving
406
- avg_biomass_per_tree = total_biomass_per_tree / total_surviving
407
- else:
408
- avg_dbh = 0
409
- avg_height = 0
410
- avg_biomass_per_tree = 0
411
- data[(sp_disp, "Surviving Trees")].append(total_surviving)
412
- data[(sp_disp, "DBH (cm)")].append(avg_dbh)
413
- data[(sp_disp, "Height (m)")].append(avg_height)
414
- data[(sp_disp, "Biomass per Tree (kg)")].append(avg_biomass_per_tree)
415
- data[(sp_disp, "Total Biomass (kg)")].append(total_biomass)
416
- columns = pd.MultiIndex.from_tuples(list(data.keys()))
417
- df = pd.DataFrame({col: data[col] for col in columns}, index=range(1, config["project"]["duration_years"] + 1))
418
- df.index.name = "Year"
419
  # Format numbers
420
  for col in df.columns:
421
  if "Surviving Trees" in col:
@@ -481,7 +400,7 @@ with gr.Blocks() as demo:
481
  for col in species_results_fmt.columns:
482
  if species_results_fmt[col].dtype in [float, int]:
483
  species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
484
- return plots[0], plots[2], plots[1], growth_fig, summary, results_fmt, species_results_fmt, survival_table, biomass_debug_df
485
 
486
  update_btn.click(
487
  update_declining_increment,
 
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
 
 
44
  def create_survival_table(model):
45
  """
46
  Returns a DataFrame with years as rows and columns for each species and total surviving trees.
47
+ Uses model.species_metrics for all values.
48
  """
49
+ df = model.species_metrics.copy()
50
+ # Pivot to wide format: years as rows, species as columns
51
+ surv = df.pivot(index="Year", columns="Species", values="Surviving Trees")
52
+ # Rename columns to display names
53
+ surv.columns = [SPECIES_DISPLAY_NAMES.get(sp, sp) for sp in surv.columns]
54
+ # Add total surviving trees column
55
+ surv["Total Surviving Trees"] = surv.sum(axis=1)
56
+ # Format numbers
57
+ for col in surv.columns:
58
+ surv[col] = surv[col].apply(lambda x: f"{x:,.0f}")
59
+ surv = surv.reset_index()
60
+ return surv
 
 
 
 
 
 
 
 
 
 
61
 
62
  def run_model_from_config(config):
63
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
 
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):
 
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:
 
400
  for col in species_results_fmt.columns:
401
  if species_results_fmt[col].dtype in [float, int]:
402
  species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
403
+ return plots[0], plots[1], plots[2], growth_fig, summary, results_fmt, species_results_fmt, survival_table, biomass_debug_df
404
 
405
  update_btn.click(
406
  update_declining_increment,
src/er_model.py CHANGED
@@ -13,6 +13,11 @@ import warnings
13
  from .allometry import calculate_biomass
14
  from .metrics import calculate_carbon
15
 
 
 
 
 
 
16
 
17
  @dataclass
18
  class Species:
@@ -246,6 +251,7 @@ class ERModel:
246
  years = range(1, self.project.duration_years + 1)
247
  results = []
248
  species_results = []
 
249
  for year in years:
250
  year_results = {"year": year}
251
  species_year_results = {"Year": year}
@@ -253,17 +259,57 @@ class ERModel:
253
  cumulative_area = self.calculate_cumulative_area(year)
254
  for species in self.species:
255
  species_carbon = 0
 
 
 
 
 
 
 
256
  for planting_year, area in self.project.planting_schedule.items():
257
  py = int(planting_year.split("_")[1])
258
  cohort_age = year - py + 1
259
  if cohort_age < 1:
260
  continue
261
- # Use correct cohort age for all calculations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  carbon = self.calculate_carbon_for_species(species, year, area, cohort_age)
263
  species_carbon += carbon
264
  total_carbon += species_carbon
265
  species_key = f"{species.name} tCO2"
266
  species_year_results[species_key] = species_carbon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  species_year_results["Total tCO2"] = total_carbon
268
  species_year_results["Cumulative ha"] = cumulative_area
269
  species_year_results["tCO2/ha"] = total_carbon / cumulative_area if cumulative_area > 0 else 0
@@ -289,6 +335,7 @@ class ERModel:
289
  species_results.append(species_year_results)
290
  self.results = pd.DataFrame(results)
291
  self.species_results = pd.DataFrame(species_results)
 
292
  return self.results, self.species_results
293
 
294
  def save_results(self, output_path: Path) -> None:
@@ -307,19 +354,22 @@ class ERModel:
307
  Returns the correct growth function and parameter dict for the given species and dimension (dbh or height).
308
  """
309
  if growth_model == "linear":
310
- func = ERModel.linear_growth
 
311
  params = species.linear[dim]
312
  elif growth_model == "linear_plateau":
313
- func = ERModel.linear_plateau_growth
 
314
  params = species.linear_plateau[dim]
315
  elif growth_model == "declining_increment":
316
  if getattr(self, 'continuous_growth', False):
317
- func = ERModel.continuous_declining_increment_growth
318
  else:
319
- func = ERModel.declining_increment_growth
320
  params = species.declining_increment[dim]
321
  else:
322
- func = ERModel.chapman_richards_growth
 
323
  params = species.chapman_richards[dim]
324
  return func, params
325
 
 
13
  from .allometry import calculate_biomass
14
  from .metrics import calculate_carbon
15
 
16
+ # Growth model imports
17
+ from .growth_models.declining_increment import declining_increment_growth, continuous_declining_increment_growth
18
+ # from growth_models.chapman_richards import chapman_richards_growth # ARCHIVED/NOT IN USE
19
+ # from growth_models.linear import linear_growth, linear_plateau_growth # ARCHIVED/NOT IN USE
20
+
21
 
22
  @dataclass
23
  class Species:
 
251
  years = range(1, self.project.duration_years + 1)
252
  results = []
253
  species_results = []
254
+ species_metrics_rows = [] # For new per-year, per-species metrics
255
  for year in years:
256
  year_results = {"year": year}
257
  species_year_results = {"Year": year}
 
259
  cumulative_area = self.calculate_cumulative_area(year)
260
  for species in self.species:
261
  species_carbon = 0
262
+ # --- New metrics ---
263
+ total_surviving = 0
264
+ total_dbh = 0
265
+ total_height = 0
266
+ total_biomass_per_tree = 0
267
+ total_biomass = 0
268
+ n_cohorts = 0
269
  for planting_year, area in self.project.planting_schedule.items():
270
  py = int(planting_year.split("_")[1])
271
  cohort_age = year - py + 1
272
  if cohort_age < 1:
273
  continue
274
+ initial_trees = species.planting_density * area
275
+ plateau_density = species.planting_density * area if cohort_age >= 5 else None
276
+ surviving = self.calculate_cohort_surviving_trees(1, cohort_age, initial_trees, species, plateau_density, self.growth_model)
277
+ dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
278
+ height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
279
+ dbh = dbh_func(cohort_age, dbh_params, species.initial_values["dbh"])
280
+ height = height_func(cohort_age, height_params, species.initial_values["height"])
281
+ biomass_per_tree = calculate_biomass(dbh, height, species.name, species.allometry)
282
+ total_surviving += surviving
283
+ total_dbh += dbh * surviving
284
+ total_height += height * surviving
285
+ total_biomass_per_tree += biomass_per_tree * surviving
286
+ total_biomass += biomass_per_tree * surviving
287
+ n_cohorts += surviving
288
+ # --- End new metrics ---
289
+ # Existing carbon calculation
290
  carbon = self.calculate_carbon_for_species(species, year, area, cohort_age)
291
  species_carbon += carbon
292
  total_carbon += species_carbon
293
  species_key = f"{species.name} tCO2"
294
  species_year_results[species_key] = species_carbon
295
+ # Store per-year, per-species metrics
296
+ if total_surviving > 0:
297
+ avg_dbh = total_dbh / total_surviving
298
+ avg_height = total_height / total_surviving
299
+ avg_biomass_per_tree = total_biomass_per_tree / total_surviving
300
+ else:
301
+ avg_dbh = 0
302
+ avg_height = 0
303
+ avg_biomass_per_tree = 0
304
+ species_metrics_rows.append({
305
+ "Year": year,
306
+ "Species": species.name,
307
+ "Surviving Trees": total_surviving,
308
+ "DBH (cm)": avg_dbh,
309
+ "Height (m)": avg_height,
310
+ "Biomass per Tree (kg)": avg_biomass_per_tree,
311
+ "Total Biomass (kg)": total_biomass
312
+ })
313
  species_year_results["Total tCO2"] = total_carbon
314
  species_year_results["Cumulative ha"] = cumulative_area
315
  species_year_results["tCO2/ha"] = total_carbon / cumulative_area if cumulative_area > 0 else 0
 
335
  species_results.append(species_year_results)
336
  self.results = pd.DataFrame(results)
337
  self.species_results = pd.DataFrame(species_results)
338
+ self.species_metrics = pd.DataFrame(species_metrics_rows)
339
  return self.results, self.species_results
340
 
341
  def save_results(self, output_path: Path) -> None:
 
354
  Returns the correct growth function and parameter dict for the given species and dimension (dbh or height).
355
  """
356
  if growth_model == "linear":
357
+ # from growth_models.linear import linear_growth
358
+ func = None # ARCHIVED/NOT IN USE
359
  params = species.linear[dim]
360
  elif growth_model == "linear_plateau":
361
+ # from growth_models.linear import linear_plateau_growth
362
+ func = None # ARCHIVED/NOT IN USE
363
  params = species.linear_plateau[dim]
364
  elif growth_model == "declining_increment":
365
  if getattr(self, 'continuous_growth', False):
366
+ func = continuous_declining_increment_growth
367
  else:
368
+ func = declining_increment_growth
369
  params = species.declining_increment[dim]
370
  else:
371
+ # from growth_models.chapman_richards import chapman_richards_growth
372
+ func = None # ARCHIVED/NOT IN USE
373
  params = species.chapman_richards[dim]
374
  return func, params
375
 
src/growth_models/chapman_richards.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chapman-Richards Growth Model (ARCHIVED/NOT IN USE)
3
+ """
4
+ import numpy as np
5
+
6
+ def chapman_richards_growth(age: float, params: dict, initial_value: float) -> float:
7
+ """
8
+ Chapman-Richards growth function.
9
+ """
10
+ a, b, c = params["a"], params["b"], params["c"]
11
+ return initial_value + (a - initial_value) * (1 - np.exp(-b * age)) ** c
src/growth_models/declining_increment.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Declining Increment Growth Model (ACTIVE)
3
+
4
+ Contains both discrete and continuous forms.
5
+ """
6
+ import numpy as np
7
+
8
+ def declining_increment_growth(age: float, params: dict, initial_value: float) -> float:
9
+ """
10
+ Discrete declining increment growth: sum annual increments, never negative.
11
+ """
12
+ r0 = params["r0"]
13
+ T_m = params["T_m"]
14
+ total = initial_value
15
+ for i in range(1, int(np.floor(age)) + 1):
16
+ increment = r0 * max(0, 1 - (i - 1) / T_m)
17
+ total += increment
18
+ frac = age - int(np.floor(age))
19
+ if frac > 0:
20
+ i = int(np.floor(age)) + 1
21
+ increment = r0 * max(0, 1 - (i - 1) / T_m)
22
+ total += frac * increment
23
+ return total
24
+
25
+ def continuous_declining_increment_growth(age: float, params: dict, initial_value: float) -> float:
26
+ """
27
+ Continuous declining increment growth: analytical formula.
28
+ """
29
+ r0 = params["r0"]
30
+ T_m = params["T_m"]
31
+ return initial_value + r0 * (age - age**2 / (2 * T_m))
src/growth_models/linear.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Linear and Linear Plateau Growth Models (ARCHIVED/NOT IN USE)
3
+ """
4
+
5
+ def linear_growth(age: float, params: dict, initial_value: float) -> float:
6
+ """
7
+ Simple linear growth function.
8
+ """
9
+ r = params["r"]
10
+ return initial_value + r * age
11
+
12
+ def linear_plateau_growth(age: float, params: dict, initial_value: float) -> float:
13
+ """
14
+ Linear growth with plateau.
15
+ """
16
+ r = params["r"]
17
+ T_p = params["T_p"]
18
+ a = params["a"]
19
+ if age <= T_p:
20
+ return initial_value + r * age
21
+ else:
22
+ return initial_value + a