malcolmSQ commited on
Commit
2f8199f
·
1 Parent(s): 316cc25

chore: UI/UX cleanup, grouped accordions, tooltips as labels, compact mortality layout, reset button

Browse files
.cursor/rules/agent.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ The agent should only request explicit user permission to complete a suggested code change if the change is large, potentially destructive, or could have significant side effects. For most routine, incremental, or clearly safe changes, the agent should proceed and apply the change without asking for additional user approval.
2
+
3
+ Summary of intent:
4
+ - Small, routine, or obviously safe changes: Apply directly, no need to ask.
5
+ - Large, sweeping, or potentially risky changes: Ask for user confirmation before proceeding.
dashboard/plots/__init__.py CHANGED
@@ -2,14 +2,17 @@
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):
@@ -24,12 +27,12 @@ def create_growth_increment_plots(config, model_type=None):
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'
@@ -40,21 +43,21 @@ def create_growth_increment_plots(config, model_type=None):
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)")
@@ -109,7 +112,7 @@ def create_all_plots(results, species_results, config):
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
 
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_objects as go
6
  from plotly.subplots import make_subplots
7
  import numpy as np
8
  import warnings
9
  import traceback
10
+ from er_model_core.allometry import calculate_biomass
11
+ from er_model_core.er_model import ERModel
12
  from pathlib import Path
13
+ from er_model_core.growth_models.chapman_richards import chapman_richards_growth
14
+ from er_model_core.growth_models.linear import linear_growth, linear_plateau_growth
15
+ from er_model_core.growth_models.declining_increment import declining_increment_growth, continuous_declining_increment_growth
16
 
17
  # Helper for checking complex numbers (copied from app.py)
18
  def check_complex(arr, label):
 
27
  model_type: 'chapman_richards', 'linear', 'linear_plateau', or 'declining_increment'
28
  """
29
  if model_type is None:
30
+ model_type = config.get('growth_model', 'Unknown Model')
31
  N = config["project"]["duration_years"]
32
  ages = np.arange(0, N + 1) # 0 to N
33
  ages_inc = np.arange(1, N + 1) # 1 to N
34
  fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
35
+ from er_model_core.er_model import ERModel
36
  SPECIES_DISPLAY_NAMES = {
37
  'species_A': 'Rhizophora spp.',
38
  'species_B': 'Avicennia germinans'
 
43
  initial_dbh = sp["initial_values"]["dbh"]
44
  initial_height = sp["initial_values"]["height"]
45
  if model_type == "linear":
46
+ dbh = [linear_growth(t, sp["linear"]["dbh"], initial_dbh) for t in ages]
47
+ height = [linear_growth(t, sp["linear"]["height"], initial_height) for t in ages]
48
  elif model_type == "linear_plateau":
49
+ dbh = [linear_plateau_growth(t, sp["linear_plateau"]["dbh"], initial_dbh) for t in ages]
50
+ height = [linear_plateau_growth(t, sp["linear_plateau"]["height"], initial_height) for t in ages]
51
  elif model_type == "declining_increment":
52
  if use_continuous:
53
+ dbh = [continuous_declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
54
+ height = [continuous_declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
55
  else:
56
+ dbh = [declining_increment_growth(t, sp["declining_increment"]["dbh"], initial_dbh) for t in ages]
57
+ height = [declining_increment_growth(t, sp["declining_increment"]["height"], initial_height) for t in ages]
58
  else:
59
+ dbh = [chapman_richards_growth(t, sp["chapman_richards"]["dbh"], initial_dbh) for t in ages]
60
+ height = [chapman_richards_growth(t, sp["chapman_richards"]["height"], initial_height) for t in ages]
61
  dbh = np.array(dbh)
62
  height = np.array(height)
63
  check_complex(dbh, f"{name} DBH (growth)")
 
112
  years = results["year"]
113
  fig3 = go.Figure()
114
  import tempfile, yaml
115
+ from er_model_core.er_model import ERModel
116
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
117
  yaml.dump(config, tmp)
118
  tmp_path = tmp.name
dashboard/tables/__init__.py CHANGED
@@ -3,7 +3,8 @@ 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
 
 
3
  """
4
  # Imports will be updated as needed when moving functions from app.py
5
  import pandas as pd
6
+ import numpy as np
7
+ from er_model_core.er_model import ERModel
8
  from pathlib import Path
9
  import yaml
10
 
{src → er_model_core}/__init__.py RENAMED
@@ -2,9 +2,9 @@
2
  ER Model package for calculating carbon sequestration in mangrove projects.
3
  """
4
 
5
- from .allometry import calculate_biomass
6
- from .er_model import ERModel, Species, ProjectConfig, CarbonConfig
7
- from .metrics import calculate_carbon
8
 
9
  __all__ = [
10
  "ERModel",
 
2
  ER Model package for calculating carbon sequestration in mangrove projects.
3
  """
4
 
5
+ from er_model_core.allometry import calculate_biomass
6
+ from er_model_core.er_model import ERModel, Species, ProjectConfig, CarbonConfig
7
+ from er_model_core.metrics import calculate_carbon
8
 
9
  __all__ = [
10
  "ERModel",
{src → er_model_core}/allometry.py RENAMED
File without changes
er_model_core/config_loader.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration loading and parsing for the ER Model.
3
+ """
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Dict, List, Tuple, Optional
7
+ import yaml
8
+
9
+ # Assuming these dataclasses are defined in er_model.er_model or a shared types module.
10
+ # For now, let's duplicate them here or import them if they are easily accessible.
11
+ # Ideally, they'd be in a place like 'er_model.types'
12
+ # For this refactor, assuming they are in er_model.er_model and we'll import them.
13
+ from er_model_core.types import Species, ProjectConfig, CarbonConfig
14
+
15
+ def load_model_config(
16
+ config_path: Optional[Path] = None,
17
+ config_dict: Optional[Dict] = None
18
+ ) -> Tuple[List[Species], ProjectConfig, CarbonConfig, str, bool, Dict]:
19
+ """
20
+ Loads model configuration from a YAML file or a dictionary.
21
+
22
+ Returns:
23
+ A tuple containing:
24
+ - List of Species objects
25
+ - ProjectConfig object
26
+ - CarbonConfig object
27
+ - Growth model name (str)
28
+ - Continuous growth flag (bool)
29
+ - The raw config dictionary
30
+ """
31
+ if config_dict is not None:
32
+ cfg = config_dict
33
+ elif config_path is not None:
34
+ with open(config_path) as f:
35
+ cfg = yaml.safe_load(f)
36
+ else:
37
+ raise ValueError("Either config_path or config_dict must be provided.")
38
+
39
+ species_list = [Species(**s) for s in cfg["species"]]
40
+ project_cfg = ProjectConfig(**cfg["project"])
41
+ carbon_cfg = CarbonConfig(**cfg["carbon"])
42
+
43
+ growth_model_name = cfg.get('growth_model', 'chapman_richards')
44
+ continuous_growth_flag = cfg.get('continuous_growth', False)
45
+
46
+ return species_list, project_cfg, carbon_cfg, growth_model_name, continuous_growth_flag, cfg
{src → er_model_core}/er_model.py RENAMED
@@ -10,13 +10,15 @@ import pandas as pd
10
  import yaml
11
  import warnings
12
 
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
@@ -64,32 +66,31 @@ class ERModel:
64
  mortality, and carbon conversion factors.
65
  """
66
 
67
- def __init__(self, config_path: Path = None, config: dict = None):
68
  """
69
  Initialize the model from a YAML configuration file or a config dict.
70
  Args:
71
  config_path: Path to the YAML configuration file
72
  config: Config dict (optional)
73
  """
74
- if config is not None:
75
- cfg = config
76
- else:
77
- with open(config_path) as f:
78
- cfg = yaml.safe_load(f)
79
- self.species = [Species(**s) for s in cfg["species"]]
80
- self.project = ProjectConfig(**cfg["project"])
81
- self.carbon = CarbonConfig(**cfg["carbon"])
82
- self.scenarios = cfg.get("scenarios", {
 
 
 
 
83
  "area": 1000.0,
84
  "dbh_range": [1.0, 20.0],
85
  "height_range": [0.5, 12.0],
86
  "growth_rate_factor": 1.0
87
  })
88
- self.results: Optional[pd.DataFrame] = None
89
- self.species_results: Optional[pd.DataFrame] = None
90
- self.scenario_results: Optional[pd.DataFrame] = None
91
- self.growth_model = cfg.get('growth_model', 'chapman_richards')
92
- self.continuous_growth = cfg.get('continuous_growth', False)
93
 
94
  def calculate_cohort_surviving_trees(self, planting_year: int, current_year: int, initial_trees: float, species: Species, plateau_density: Optional[float] = None, growth_model: str = None) -> float:
95
  """
@@ -170,49 +171,45 @@ class ERModel:
170
  total += area
171
  return total
172
 
173
- @staticmethod
174
- def chapman_richards_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
175
- a, b, c = params["a"], params["b"], params["c"]
176
- return initial_value + (a - initial_value) * (1 - np.exp(-b * age)) ** c
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- @staticmethod
179
- def linear_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
180
- r = params["r"]
181
- return initial_value + r * age
182
 
183
- @staticmethod
184
- def linear_plateau_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
185
- r = params["r"]
186
- T_p = params["T_p"]
187
- a = params["a"]
188
- if age <= T_p:
189
- return initial_value + r * age
190
  else:
191
- return initial_value + a
 
 
 
 
 
192
 
193
- @staticmethod
194
- def declining_increment_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
195
- r0 = params["r0"]
196
- T_m = params["T_m"]
197
- # Accumulate annual increments, never negative
198
- total = initial_value
199
- for i in range(1, int(np.floor(age)) + 1):
200
- increment = r0 * max(0, 1 - (i - 1) / T_m)
201
- total += increment
202
- # If age is fractional, add partial increment for the last year
203
- frac = age - int(np.floor(age))
204
- if frac > 0:
205
- i = int(np.floor(age)) + 1
206
- increment = r0 * max(0, 1 - (i - 1) / T_m)
207
- total += frac * increment
208
- return total
209
 
210
- @staticmethod
211
- def continuous_declining_increment_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
212
- r0 = params["r0"]
213
- T_m = params["T_m"]
214
- # Continuous formula: initial + r0 * (age - age^2/(2*Tm))
215
- return initial_value + r0 * (age - age**2 / (2 * T_m))
216
 
217
  def calculate_carbon_for_species(self, species: Species, age: int, area: float, cohort_age: int) -> float:
218
  """
@@ -340,69 +337,9 @@ class ERModel:
340
 
341
  def save_results(self, output_path: Path) -> None:
342
  """
343
- Save results to CSV file.
344
-
345
- Args:
346
- output_path: Path to save the results CSV
347
  """
348
  if self.results is None:
349
- raise ValueError("No results available. Run the model first.")
350
- self.results.to_csv(output_path, index=False)
351
-
352
- def get_growth_function_and_params(self, species, growth_model, dim):
353
- """
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
-
376
- # --- Parameter sweep/test for plausible survival curves ---
377
- def test_dbh_mortality_sweep():
378
- import matplotlib.pyplot as plt
379
- import numpy as np
380
- m_refs = [0.01, 0.05, 0.1, 0.16]
381
- ps = [1.0, 1.5, 2.0]
382
- DBH_ref = 9.0
383
- years = np.arange(1, 31)
384
- initial_trees = 1000
385
- results = {}
386
- for m_ref in m_refs:
387
- for p in ps:
388
- N_live = initial_trees
389
- N_lives = []
390
- for year in years:
391
- dbh = 1.0 + (year - 1) * 0.5 # simple linear DBH growth for test
392
- dbh = max(dbh, 1.0)
393
- m = m_ref * (DBH_ref / dbh) ** p
394
- m = min(max(m, 0), 0.99)
395
- N_live = N_live * (1 - m)
396
- N_lives.append(N_live)
397
- results[(m_ref, p)] = N_lives
398
- plt.figure(figsize=(10,6))
399
- for (m_ref, p), N_lives in results.items():
400
- plt.plot(years, N_lives, label=f"m_ref={m_ref}, p={p}")
401
- plt.xlabel("Year")
402
- plt.ylabel("Surviving Trees")
403
- plt.title("DBH-dependent Mortality Parameter Sweep")
404
- plt.legend()
405
- plt.grid(True)
406
- plt.show()
407
-
408
- # To run the test, call test_dbh_mortality_sweep() from __main__ or a notebook.
 
10
  import yaml
11
  import warnings
12
 
13
+ from er_model_core.allometry import calculate_biomass
14
+ from er_model_core.metrics import calculate_carbon
15
+ from er_model_core.config_loader import load_model_config
16
+ from er_model_core.types import Species, ProjectConfig, CarbonConfig
17
 
18
+ # Growth model imports - ensure all are imported
19
+ from er_model_core.growth_models.chapman_richards import chapman_richards_growth
20
+ from er_model_core.growth_models.linear import linear_growth, linear_plateau_growth
21
+ from er_model_core.growth_models.declining_increment import declining_increment_growth, continuous_declining_increment_growth
22
 
23
 
24
  @dataclass
 
66
  mortality, and carbon conversion factors.
67
  """
68
 
69
+ def __init__(self, config_path: Optional[Path] = None, config: Optional[dict] = None):
70
  """
71
  Initialize the model from a YAML configuration file or a config dict.
72
  Args:
73
  config_path: Path to the YAML configuration file
74
  config: Config dict (optional)
75
  """
76
+ (
77
+ self.species,
78
+ self.project,
79
+ self.carbon,
80
+ self.growth_model,
81
+ self.continuous_growth,
82
+ self.raw_config # Storing the raw config might be useful for other parts
83
+ ) = load_model_config(config_path=config_path, config_dict=config)
84
+
85
+ self.results: Optional[pd.DataFrame] = None
86
+ self.species_results: Optional[pd.DataFrame] = None
87
+ self.scenario_results: Optional[pd.DataFrame] = None
88
+ self.scenarios = self.raw_config.get("scenarios", {
89
  "area": 1000.0,
90
  "dbh_range": [1.0, 20.0],
91
  "height_range": [0.5, 12.0],
92
  "growth_rate_factor": 1.0
93
  })
 
 
 
 
 
94
 
95
  def calculate_cohort_surviving_trees(self, planting_year: int, current_year: int, initial_trees: float, species: Species, plateau_density: Optional[float] = None, growth_model: str = None) -> float:
96
  """
 
171
  total += area
172
  return total
173
 
174
+ def get_growth_function_and_params(self, species: Species, growth_model_name: str, dim: str) -> Tuple[callable, Dict[str, float]]:
175
+ """
176
+ Get the growth function and its parameters for a given species, model, and dimension (dbh or height).
177
+ Uses self.continuous_growth to select continuous version if applicable.
178
+ """
179
+ params = species.__dict__.get(growth_model_name, {}).get(dim, {})
180
+ initial_value_key = f"initial_{dim}"
181
+ # initial_value = species.initial_values.get(dim, 0) # This was the old way, let's ensure params are complete
182
+
183
+ growth_functions = {
184
+ "chapman_richards": chapman_richards_growth,
185
+ "linear": linear_growth,
186
+ "linear_plateau": linear_plateau_growth,
187
+ "declining_increment": declining_increment_growth,
188
+ # Potentially add more models here
189
+ }
190
 
191
+ continuous_growth_functions = {
192
+ "declining_increment": continuous_declining_increment_growth,
193
+ # Add other continuous versions if they exist
194
+ }
195
 
196
+ if self.continuous_growth and growth_model_name in continuous_growth_functions:
197
+ selected_func = continuous_growth_functions[growth_model_name]
198
+ elif growth_model_name in growth_functions:
199
+ selected_func = growth_functions[growth_model_name]
 
 
 
200
  else:
201
+ # Fallback or error if model name is not found
202
+ # For now, let's use chapman_richards as a default if self.growth_model itself isn't specific enough
203
+ # However, growth_model_name parameter should be the specific one.
204
+ warnings.warn(f"Growth model '{growth_model_name}' not found or not supported. Defaulting to chapman_richards for {species.name} {dim}. Check config.")
205
+ selected_func = chapman_richards_growth # Default fallback
206
+ params = species.chapman_richards.get(dim, {}) if species.chapman_richards else {}
207
 
208
+ if not params:
209
+ warnings.warn(f"No parameters found for {growth_model_name} {dim} for species {species.name}. Growth may be incorrect.")
210
+ # Provide some default empty params if really needed, or let it fail if func expects them.
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ return selected_func, params
 
 
 
 
 
213
 
214
  def calculate_carbon_for_species(self, species: Species, age: int, area: float, cohort_age: int) -> float:
215
  """
 
337
 
338
  def save_results(self, output_path: Path) -> None:
339
  """
340
+ Save the main results DataFrame to a CSV file.
 
 
 
341
  """
342
  if self.results is None:
343
+ raise ValueError("Results have not been calculated yet. Run model.run() first.")
344
+ output_path.parent.mkdir(parents=True, exist_ok=True)
345
+ self.results.to_csv(output_path, index=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
{src → er_model_core}/growth_models/chapman_richards.py RENAMED
File without changes
{src → er_model_core}/growth_models/declining_increment.py RENAMED
File without changes
{src → er_model_core}/growth_models/linear.py RENAMED
File without changes
{src → er_model_core}/metrics.py RENAMED
File without changes
er_model_core/types.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Optional
3
+
4
+ @dataclass
5
+ class Species:
6
+ """Species-specific parameters for growth and carbon calculations."""
7
+ name: str
8
+ planting_density: float
9
+ # Old style
10
+ mortality_rates: Optional[Dict[str, float]] = None
11
+ # New style
12
+ m_ref: Optional[float] = None
13
+ DBH_ref: Optional[float] = None
14
+ p: Optional[float] = None
15
+ chapman_richards: Dict[str, Dict[str, float]] = None
16
+ allometry: Dict[str, float] = None
17
+ initial_values: Dict[str, float] = None
18
+ linear: Optional[Dict[str, Dict[str, float]]] = None
19
+ linear_plateau: Optional[Dict[str, Dict[str, float]]] = None
20
+ declining_increment: Optional[Dict[str, Dict[str, float]]] = None
21
+
22
+ @dataclass
23
+ class ProjectConfig:
24
+ """Project configuration parameters."""
25
+ duration_years: int
26
+ planting_schedule: Dict[str, float]
27
+
28
+ @dataclass
29
+ class CarbonConfig:
30
+ """Carbon conversion and adjustment parameters."""
31
+ biomass_to_carbon: float
32
+ carbon_to_co2: float
33
+ buffer_percentage: float
34
+ leakage_percentage: float
35
+ baseline_emissions: float
36
+ soil_carbon_per_ha_per_year: float = 0.0
requirements.txt CHANGED
@@ -1,5 +1,8 @@
 
1
  gradio>=3.41.2
2
- pandas>=1.5.3
3
- numpy>=1.23.0
 
4
  plotly>=5.0.0
5
- pyyaml
 
 
1
+ click>=8.1.7
2
  gradio>=3.41.2
3
+ matplotlib>=3.8.2
4
+ numpy>=1.26.3
5
+ pandas>=2.2.0
6
  plotly>=5.0.0
7
+ pyyaml>=6.0.1
8
+ scipy>=1.12.0
scripts/run_pipeline.py CHANGED
@@ -4,14 +4,13 @@ Main pipeline script for running the ER model calculations.
4
  """
5
  import sys
6
  from pathlib import Path
 
 
 
7
 
8
  # Add the project root to the Python path
9
  sys.path.append(str(Path(__file__).parent.parent))
10
 
11
- import click
12
-
13
- from src.er_model import ERModel
14
-
15
 
16
  @click.command()
17
  @click.option(
 
4
  """
5
  import sys
6
  from pathlib import Path
7
+ import yaml
8
+ import click
9
+ from er_model_core.er_model import ERModel
10
 
11
  # Add the project root to the Python path
12
  sys.path.append(str(Path(__file__).parent.parent))
13
 
 
 
 
 
14
 
15
  @click.command()
16
  @click.option(
setup.py CHANGED
@@ -3,7 +3,8 @@ from setuptools import find_packages, setup
3
  setup(
4
  name="er_model",
5
  version="0.1.0",
6
- packages=find_packages(),
 
7
  install_requires=[
8
  "pandas>=2.2.0",
9
  "numpy>=1.26.3",
 
3
  setup(
4
  name="er_model",
5
  version="0.1.0",
6
+ package_dir={'': 'er_model_core'},
7
+ packages=find_packages(where='er_model_core'),
8
  install_requires=[
9
  "pandas>=2.2.0",
10
  "numpy>=1.26.3",
tests/test_er_model.py CHANGED
@@ -6,10 +6,11 @@ from pathlib import Path
6
  import numpy as np
7
  import pytest
8
  import yaml
 
9
 
10
- from src.allometry import calculate_biomass
11
- from src.er_model import ERModel, Species
12
- from src.metrics import calculate_carbon
13
 
14
 
15
  @pytest.fixture
@@ -133,7 +134,7 @@ def test_full_pipeline(config_file):
133
 
134
  def test_dbh_dependent_mortality():
135
  """Test that DBH-dependent mortality matches expected at DBH=9 and year-5 plateau."""
136
- from src.er_model import ERModel, Species
137
  # Minimal species config
138
  species = Species(
139
  name="Test Species",
@@ -171,7 +172,7 @@ def test_dbh_dependent_mortality():
171
 
172
  def test_allometric_equation_species_A_B():
173
  """Test Zanvo et al. 2023 allometric equations for both species."""
174
- from src.allometry import calculate_biomass
175
  # Test values
176
  dbh = 10.0 # cm
177
  height = 5.0 # m
@@ -182,4 +183,73 @@ def test_allometric_equation_species_A_B():
182
  # species_B (Avicennia germinans)
183
  expected_B = 1.486 * (dbh**2 * height)**0.55864
184
  result_B = calculate_biomass(dbh, height, "species_B", {})
185
- assert np.isclose(result_B, expected_B, rtol=1e-6), f"species_B: got {result_B}, expected {expected_B}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import numpy as np
7
  import pytest
8
  import yaml
9
+ import pandas as pd
10
 
11
+ from er_model_core.allometry import calculate_biomass
12
+ from er_model_core.er_model import ERModel, Species
13
+ from er_model_core.metrics import calculate_carbon
14
 
15
 
16
  @pytest.fixture
 
134
 
135
  def test_dbh_dependent_mortality():
136
  """Test that DBH-dependent mortality matches expected at DBH=9 and year-5 plateau."""
137
+ from er_model_core.er_model import ERModel, Species
138
  # Minimal species config
139
  species = Species(
140
  name="Test Species",
 
172
 
173
  def test_allometric_equation_species_A_B():
174
  """Test Zanvo et al. 2023 allometric equations for both species."""
175
+ from er_model_core.allometry import calculate_biomass
176
  # Test values
177
  dbh = 10.0 # cm
178
  height = 5.0 # m
 
183
  # species_B (Avicennia germinans)
184
  expected_B = 1.486 * (dbh**2 * height)**0.55864
185
  result_B = calculate_biomass(dbh, height, "species_B", {})
186
+ assert np.isclose(result_B, expected_B, rtol=1e-6), f"species_B: got {result_B}, expected {expected_B}"
187
+
188
+ # Test with a known allometry to check biomass calculation linkage
189
+ from er_model_core.allometry import calculate_biomass
190
+
191
+ # Basic check that biomass is positive for reasonable inputs
192
+ # ... existing code ...
193
+
194
+ # Re-import ERModel and Species for this specific test to ensure clean state if needed
195
+ from er_model_core.er_model import ERModel, Species
196
+ model = ERModel(config=config_dict)
197
+ results_df, species_results_df = model.run()
198
+
199
+ # ... existing code ...
200
+
201
+ # ... existing code ...
202
+
203
+ # --- Parameter sweep/test for plausible survival curves (moved from er_model.py) ---
204
+ def test_dbh_mortality_sweep():
205
+ import matplotlib.pyplot as plt
206
+ import numpy as np
207
+ m_refs = [0.01, 0.05, 0.1, 0.16]
208
+ ps = [1.0, 1.5, 2.0]
209
+ DBH_ref = 9.0
210
+ years = np.arange(1, 31)
211
+ initial_trees = 1000
212
+ results = {}
213
+ for m_ref in m_refs:
214
+ for p in ps:
215
+ N_live = initial_trees
216
+ N_lives = []
217
+ for year in years:
218
+ # This test requires a simplified DBH growth for its internal logic.
219
+ # It does not use the main ERModel's growth functions directly.
220
+ dbh_test_growth = 1.0 + (year - 1) * 0.5 # simple linear DBH growth for test
221
+ dbh = max(dbh_test_growth, 1.0)
222
+
223
+ # Mortality calculation as per ERModel logic being tested
224
+ m = m_ref * (DBH_ref / dbh) ** p
225
+ m = min(max(m, 0), 0.99)
226
+ N_live = N_live * (1 - m)
227
+ N_lives.append(N_live)
228
+ results[(m_ref, p)] = N_lives
229
+
230
+ # This test originally had plotting. For automated tests, assertions are better.
231
+ # If visual inspection is needed, this part can be run in a notebook.
232
+ # For now, let's assert that the number of trees is non-increasing.
233
+ for (m_ref, p), N_lives_curve in results.items():
234
+ for i in range(len(N_lives_curve) - 1):
235
+ assert N_lives_curve[i+1] <= N_lives_curve[i], f"Mortality sweep {m_ref, p}: Trees increased from {N_lives_curve[i]} to {N_lives_curve[i+1]}"
236
+ assert N_lives_curve[-1] < initial_trees, f"Mortality sweep {m_ref, p}: No mortality occurred or trees increased."
237
+
238
+ # plt.figure(figsize=(10,6))
239
+ # for (m_ref, p), N_lives_plot in results.items():
240
+ # plt.plot(years, N_lives_plot, label=f"m_ref={m_ref}, p={p}")
241
+ # plt.xlabel("Year")
242
+ # plt.ylabel("Surviving Trees")
243
+ # plt.title("DBH-dependent Mortality Parameter Sweep")
244
+ # plt.legend()
245
+ # plt.grid(True)
246
+ # To save plot instead of showing:
247
+ # output_dir = Path(__file__).parent.parent / "outputs" / "test_plots"
248
+ # output_dir.mkdir(parents=True, exist_ok=True)
249
+ # plt.savefig(output_dir / "dbh_mortality_sweep.png")
250
+ # plt.close()
251
+ print("test_dbh_mortality_sweep completed basic assertions.")
252
+
253
+ # If you want to run this specific test and see the plot (e.g., during development):
254
+ # if __name__ == "__main__":
255
+ # test_dbh_mortality_sweep() # You would need to handle imports for ERModel parts if called directly here