malcolmSQ commited on
Commit
fe152d4
·
0 Parent(s):

Initial commit: ER model for Nigeria mangroves

Browse files
.cursor/rules/activate_env.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ name: Activate er-model environment
2
+ description: Ensures the er-model conda environment is activated before running any script
3
+ events:
4
+ - before_run_script
5
+ actions:
6
+ - name: Activate conda environment
7
+ command: conda activate er-model
8
+ shell: true
9
+ continue_on_error: false
.cursor/rules/code_quality.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Code Quality Standards
2
+ description: Enforces code style and quality standards for the ER model project
3
+ events:
4
+ - before_save
5
+ - before_commit
6
+ actions:
7
+ - name: Format with black
8
+ command: black {file}
9
+ shell: true
10
+ continue_on_error: true
11
+
12
+ - name: Sort imports
13
+ command: isort {file}
14
+ shell: true
15
+ continue_on_error: true
16
+
17
+ - name: Check type hints
18
+ command: mypy {file}
19
+ shell: true
20
+ continue_on_error: true
21
+
22
+ globs:
23
+ - "**/*.py"
24
+
25
+ exclude:
26
+ - "**/venv/**"
27
+ - "**/.env/**"
28
+ - "**/build/**"
29
+ - "**/dist/**"
.cursor/rules/er_model_guidelines.md ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Guidelines for the ER Model Python Repository
2
+ # Scope: applies to all Python files in this project
3
+
4
+ description: |
5
+ Provide persistent, project‑level guidance for converting the Jupyter-based ER model
6
+ into a lightweight, reproducible Python repo with standard environment management,
7
+ modular scripts, analytics dashboard, and testing. Enforces code style, structure,
8
+ and workflows for maintainability and clarity.
9
+ alwaysApply: true
10
+ globs:
11
+ - "**/*.py"
12
+
13
+ # Environment
14
+ - Use Miniconda/Miniforge to manage the Python environment.
15
+ - Define all dependencies in an `environment.yml` at the project root:
16
+ ```yaml
17
+ name: er-model
18
+ channels:
19
+ - conda-forge
20
+ dependencies:
21
+ - python>=3.10
22
+ - pandas
23
+ - numpy
24
+ - scipy
25
+ - matplotlib
26
+ - streamlit
27
+ - pytest
28
+ - black
29
+ - isort
30
+ ```
31
+
32
+ # Repository Structure
33
+ - `src/` → core modules (e.g. `er_model.py`, `allometry.py`, `metrics.py`).
34
+ - `scripts/` → CLI entrypoints (e.g. `run_pipeline.py`, `analyze_results.py`).
35
+ - `dashboard/` → analytics dashboard (e.g. `app.py` for Streamlit).
36
+ - `tests/` → pytest test suites.
37
+ - `configs/` → default parameter YAML or JSON files.
38
+ - `README.md` → project overview, setup, and usage.
39
+
40
+ # Code Style & Quality
41
+ - Follow PEP8 conventions; enforce with `black --check` and `isort --check`.
42
+ - Use Python type hints on all public functions.
43
+ - Document functions with docstrings (Google style).
44
+ - Avoid notebooks; use scripts and modules only.
45
+
46
+ # Workflow & Tooling
47
+ - **Parameterization:** Accept inputs via a single YAML/JSON config or CLI flags.
48
+ - **Pipeline:** `scripts/run_pipeline.py` should:
49
+ 1. Load config or parse flags.
50
+ 2. Execute steps in order (data load → allometry → CR growth → results).
51
+ 3. Save outputs (CSV, figures) to `outputs/`.
52
+ - **Testing:** Write pytest tests targeting each core function. Place them under `tests/`.
53
+ - **Formatting checks:** Add a Git pre-commit hook to run black, isort, and pytest.
54
+
55
+ # Dashboard
56
+ - Use Streamlit in `dashboard/app.py`:
57
+ - Load results from `outputs/`.
58
+ - Provide sidebar controls for key parameters (planting density, mortality, a/b/c values).
59
+ - Display tables, plots, and key summary metrics.
60
+
61
+ # CI/CD (optional)
62
+ - Configure GitHub Actions to:
63
+ 1. Create conda environment from `environment.yml`.
64
+ 2. Run formatting checks, linting, tests.
65
+ 3. Deploy Streamlit dashboard (if needed).
66
+
67
+
68
+ # conda env
69
+ - Always activate our er-model conda environment before running commandsa
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Jupyter Notebook
24
+ .ipynb_checkpoints
25
+
26
+ # Environment
27
+ .env
28
+ .venv
29
+ env/
30
+ venv/
31
+ ENV/
32
+
33
+ # IDE
34
+ .idea/
35
+ .vscode/
36
+ *.swp
37
+ *.swo
38
+
39
+ # Project specific
40
+ outputs/
41
+ *.log
42
+ .DS_Store
.pre-commit-config.yaml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.5.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-added-large-files
9
+
10
+ - repo: https://github.com/psf/black
11
+ rev: 24.2.0
12
+ hooks:
13
+ - id: black
14
+ language_version: python3.10
15
+
16
+ - repo: https://github.com/pycqa/isort
17
+ rev: 5.13.2
18
+ hooks:
19
+ - id: isort
20
+ args: ["--profile", "black"]
21
+
22
+ - repo: https://github.com/pre-commit/mirrors-mypy
23
+ rev: v1.8.0
24
+ hooks:
25
+ - id: mypy
26
+ additional_dependencies: [types-all]
27
+ args: [--ignore-missing-imports]
README.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ER Model - Nigeria Mangroves
2
+
3
+ A Python implementation of the Emissions Reduction (ER) model for Nigeria mangrove projects. This repository provides tools to estimate carbon sequestration over a 30-year period based on mangrove growth and survival rates.
4
+
5
+ ## Recent Updates and Improvements
6
+
7
+ ### Growth Model Enhancements (2024)
8
+ - **Updated Allometric Equations**: Implemented Zanvo et al. (2023) species-specific equations:
9
+ - Rhizophora: Total = 1.938 × (DBH² H)^0.67628
10
+ - Avicennia: Total = 1.486 × (DBH² H)^0.55864
11
+
12
+ - **Chapman-Richards Implementation**:
13
+ - Improved growth parameter estimation
14
+ - Direct use of growth rate parameter (b) instead of deriving from Tm
15
+ - Separate parameterization for DBH and height growth
16
+ - Current parameters:
17
+ * Rhizophora: max DBH 11.07cm, max height 12.0m
18
+ * Avicennia: max DBH 17.0cm, max height 8.8m
19
+
20
+ - **Growth Rate Parameters**:
21
+ - b = 0.25 yr^-1 (independently estimated growth rate)
22
+ - c = 2.1 (shape parameter)
23
+ - Removed Tm-based calculation (previously b = ln(c)/Tm)
24
+
25
+ ### Dashboard Improvements
26
+ - Added height growth visualization
27
+ - Improved number formatting with comma separators
28
+ - Enhanced scenario comparison features
29
+ - Added biomass per tree tracking
30
+ - Updated parameter input ranges
31
+
32
+ ### Configuration Updates
33
+ - Restructured parameter files for clarity
34
+ - Added detailed parameter documentation
35
+ - Improved error handling for invalid inputs
36
+ - Added support for scenario analysis
37
+
38
+ ## Features
39
+
40
+ - Species-specific growth modeling using Chapman-Richards equations
41
+ - Biomass estimation using allometric equations
42
+ - Carbon conversion and emissions reduction calculations
43
+ - Interactive dashboard for parameter exploration using Gradio
44
+ - Configurable planting schedules and mortality rates
45
+ - Scenario comparison for different DBH ranges and growth rates
46
+ - Per-species carbon tracking and cumulative hectare calculations
47
+
48
+ ## Model Parameters
49
+
50
+ ### Species Parameters
51
+ - **Rhizophora**
52
+ - Initial values: DBH = 1.0cm, Height = 0.5m
53
+ - Chapman-Richards parameters: a = 11.07cm, b = 0.35, c = 2.1
54
+ - Configurable planting density and mortality rates
55
+
56
+ - **Avicennia**
57
+ - Initial values: DBH = 1.0cm, Height = 0.5m
58
+ - Chapman-Richards parameters: a = 17.0cm, b = 0.35, c = 2.1
59
+ - Configurable planting density and mortality rates
60
+
61
+ ### Carbon Parameters
62
+ - Biomass to carbon conversion factor
63
+ - Carbon to CO2 conversion factor
64
+ - Buffer pool percentage
65
+ - Leakage percentage
66
+ - Baseline emissions rate
67
+
68
+ ## Installation
69
+
70
+ 1. Install [Miniconda](https://docs.conda.io/en/latest/miniconda.html) or [Miniforge](https://github.com/conda-forge/miniforge)
71
+
72
+ 2. Create and activate the environment:
73
+ ```bash
74
+ conda env create -f environment.yml
75
+ conda activate er-model
76
+ ```
77
+
78
+ 3. Install pre-commit hooks:
79
+ ```bash
80
+ pre-commit install
81
+ ```
82
+
83
+ ## Project Structure
84
+
85
+ - `src/` - Core model implementation
86
+ - `er_model.py` - Main ER calculation logic
87
+ - `allometry.py` - Species-specific allometric equations
88
+ - `metrics.py` - Carbon conversion and statistics
89
+ - `scripts/` - Command-line tools
90
+ - `run_pipeline.py` - Execute full ER calculation workflow
91
+ - `analyze_results.py` - Generate reports and visualizations
92
+ - `dashboard/` - Gradio web application for interactive exploration
93
+ - `configs/` - Default parameter files
94
+ - `outputs/` - Generated results and figures
95
+ - `tests/` - Test suite
96
+
97
+ ## Dashboard Features
98
+
99
+ The interactive Gradio dashboard allows users to:
100
+ 1. Adjust species-specific parameters:
101
+ - Planting density (trees/ha)
102
+ - Year-by-year mortality rates
103
+ 2. Configure planting schedule:
104
+ - Area planted per year (ha)
105
+ - Up to 5-year planting schedule
106
+ 3. Modify carbon parameters:
107
+ - Buffer pool percentage
108
+ 4. Explore scenarios:
109
+ - Test different DBH ranges
110
+ - Adjust growth rate factors
111
+ - Compare results across 1000 ha scenarios
112
+ 5. View results:
113
+ - Carbon sequestration over time
114
+ - Milestone year comparisons
115
+ - Per-species carbon tracking
116
+ - Cumulative hectare metrics
117
+ - Scenario comparison tables
118
+
119
+ ## Usage
120
+
121
+ 1. Configure parameters in `configs/params.yaml`
122
+
123
+ 2. Run the full pipeline:
124
+ ```bash
125
+ python scripts/run_pipeline.py --config configs/params.yaml
126
+ ```
127
+
128
+ 3. Launch the dashboard:
129
+ ```bash
130
+ python dashboard/app.py
131
+ ```
132
+ This will start a Gradio server and open the dashboard in your default web browser.
133
+
134
+ ## Development
135
+
136
+ - Code style is enforced using `black` and `isort`
137
+ - Type hints are required for all public functions
138
+ - Tests are written using `pytest`
139
+ - Pre-commit hooks ensure code quality
140
+
141
+ ## Dependencies
142
+
143
+ Key dependencies (specified in `environment.yml`):
144
+ - numpy=1.24.3
145
+ - pandas=2.0.3
146
+ - gradio (latest version)
147
+ - matplotlib
148
+ - pyyaml
149
+ - pytest (for development)
150
+
151
+ ## License
152
+
153
+ [License details to be added]
configs/params.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ species:
2
+ - name: "species_A" # Rhizophora spp.
3
+ planting_density: 4000 # trees/ha
4
+ mortality_rates:
5
+ year_1: 15 # %
6
+ year_2: 12 # %
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"
22
+ initial_values:
23
+ dbh: 1.0 # cm
24
+ height: 0.5 # m
25
+
26
+ - name: "species_B" # Avicennia germinans
27
+ planting_density: 444 # trees/ha
28
+ mortality_rates:
29
+ year_1: 15 # %
30
+ year_2: 12 # %
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"
46
+ initial_values:
47
+ dbh: 1.0 # cm
48
+ height: 0.5 # m
49
+
50
+ 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
58
+
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
+
66
+ # Note: b parameter is calculated using b = ln(c) / Tm
67
+ # For both species:
68
+ # - Tm = 6 years
69
+ # - c = 2.1
70
+ # Therefore: b = ln(2.1) / 6 ≈ 0.35
71
+
72
+ scenarios:
73
+ area: 1000.0 # ha
74
+ dbh_range: [1.0, 25.0] # cm
75
+ height_range: [0.5, 15.0] # m
76
+ growth_rate_factor: 1.0
dashboard/app.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio dashboard for exploring ER model results and parameters.
3
+ """
4
+ from pathlib import Path
5
+ from typing import Dict, List, Tuple
6
+
7
+ import gradio as gr
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import pandas as pd
11
+ import yaml
12
+
13
+ from src.er_model import ERModel
14
+
15
+
16
+ def load_config(config_path: Path = Path("configs/params.yaml")) -> dict:
17
+ """Load and parse YAML configuration."""
18
+ with open(config_path) as f:
19
+ return yaml.safe_load(f)
20
+
21
+
22
+ def run_model(config: dict) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
23
+ """Run model with current parameters."""
24
+ # Save temporary config
25
+ temp_config = Path("configs/temp_config.yaml")
26
+ with open(temp_config, "w") as f:
27
+ yaml.dump(config, f)
28
+
29
+ # Run model and cleanup
30
+ model = ERModel(temp_config)
31
+ results, species_results, scenario_results = model.run()
32
+ temp_config.unlink()
33
+
34
+ return results, species_results, scenario_results
35
+
36
+
37
+ def format_number(x):
38
+ """Format number with comma separators, handling NaN and floating points."""
39
+ if pd.isna(x):
40
+ return "0"
41
+ if isinstance(x, (int, float)):
42
+ if isinstance(x, int) or float(x).is_integer():
43
+ return format(int(x), ',')
44
+ return format(round(float(x), 2), ',')
45
+ return str(x)
46
+
47
+
48
+ def create_plots_and_tables(
49
+ results: pd.DataFrame,
50
+ species_results: pd.DataFrame,
51
+ scenario_results: pd.DataFrame
52
+ ) -> Tuple[plt.Figure, plt.Figure, plt.Figure, str, pd.DataFrame, pd.DataFrame]:
53
+ """Create carbon sequestration plots, summary, and tables."""
54
+ # Clear any existing plots
55
+ plt.close('all')
56
+
57
+ # Time series plot
58
+ fig1 = plt.figure(figsize=(10, 6))
59
+ ax1 = fig1.add_subplot(111)
60
+ results.plot(
61
+ x="year",
62
+ y=["gross_carbon", "net_carbon"],
63
+ ax=ax1,
64
+ title="Carbon Sequestration Over Time"
65
+ )
66
+ ax1.set_xlabel("Year")
67
+ ax1.set_ylabel("Carbon (tCO2)")
68
+ ax1.legend(["Gross Carbon", "Net Carbon"])
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"))
124
+
125
+ # Get final year carbon values
126
+ final_gross = results["gross_carbon"].iloc[-1]
127
+ final_net = results["net_carbon"].iloc[-1]
128
+
129
+ # Calculate per hectare metrics
130
+ if total_area > 0:
131
+ gross_carbon_per_ha = final_gross / total_area
132
+ net_carbon_per_ha = final_net / total_area
133
+ else:
134
+ gross_carbon_per_ha = 0
135
+ net_carbon_per_ha = 0
136
+
137
+ # Create summary text with formatted numbers
138
+ summary = f"""
139
+ Results Summary:
140
+ ---------------
141
+ Total Years: {len(results)}
142
+ Total Area Planted: {format_number(total_area)} ha
143
+ Total Gross Carbon: {format_number(final_gross)} tCO2
144
+ Total Net Carbon: {format_number(final_net)} tCO2
145
+
146
+ Per Hectare Metrics (Year 30):
147
+ -----------------------------
148
+ Gross Carbon per ha: {format_number(gross_carbon_per_ha)} tCO2/ha
149
+ Net Carbon per ha: {format_number(net_carbon_per_ha)} tCO2/ha
150
+ """
151
+
152
+ # Format species results table with comma separators
153
+ species_table = species_results.copy()
154
+ species_table.index.name = "Year"
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(
168
+ rhiz_density: float,
169
+ rhiz_mort_1: float,
170
+ rhiz_mort_2: float,
171
+ rhiz_mort_3: float,
172
+ rhiz_mort_4: float,
173
+ rhiz_mort_5: float,
174
+ avic_density: float,
175
+ avic_mort_1: float,
176
+ avic_mort_2: float,
177
+ avic_mort_3: float,
178
+ avic_mort_4: float,
179
+ avic_mort_5: float,
180
+ year_1_area: float,
181
+ year_2_area: float,
182
+ year_3_area: float,
183
+ year_4_area: float,
184
+ year_5_area: float,
185
+ buffer_percentage: float,
186
+ scenario_area: float = 1000.0,
187
+ dbh_range_min: float = 1.0,
188
+ dbh_range_max: float = 25.0,
189
+ height_range_min: float = 0.5,
190
+ height_range_max: float = 15.0,
191
+ growth_rate_factor: float = 1.0,
192
+ ) -> Tuple[plt.Figure, plt.Figure, plt.Figure, str, pd.DataFrame, pd.DataFrame]:
193
+ """Update model with new parameters and return plots and tables."""
194
+ # Load base config
195
+ config = load_config()
196
+
197
+ # Update Rhizophora parameters
198
+ config["species"][0]["planting_density"] = rhiz_density
199
+ config["species"][0]["mortality_rates"].update({
200
+ "year_1": rhiz_mort_1,
201
+ "year_2": rhiz_mort_2,
202
+ "year_3": rhiz_mort_3,
203
+ "year_4": rhiz_mort_4,
204
+ "year_5": rhiz_mort_5
205
+ })
206
+
207
+ # Update Avicennia parameters
208
+ config["species"][1]["planting_density"] = avic_density
209
+ config["species"][1]["mortality_rates"].update({
210
+ "year_1": avic_mort_1,
211
+ "year_2": avic_mort_2,
212
+ "year_3": avic_mort_3,
213
+ "year_4": avic_mort_4,
214
+ "year_5": avic_mort_5
215
+ })
216
+
217
+ # Update planting schedule
218
+ config["project"]["planting_schedule"].update({
219
+ "year_1": year_1_area,
220
+ "year_2": year_2_area,
221
+ "year_3": year_3_area,
222
+ "year_4": year_4_area,
223
+ "year_5": year_5_area
224
+ })
225
+
226
+ # Update carbon parameters
227
+ config["carbon"]["buffer_percentage"] = buffer_percentage
228
+
229
+ # Add scenario parameters
230
+ config["scenarios"] = {
231
+ "area": scenario_area,
232
+ "dbh_range": [dbh_range_min, dbh_range_max],
233
+ "height_range": [height_range_min, height_range_max],
234
+ "growth_rate_factor": growth_rate_factor
235
+ }
236
+
237
+ # Run model and create plots/tables
238
+ results, species_results, scenario_results = run_model(config)
239
+ return create_plots_and_tables(results, species_results, scenario_results)
240
+
241
+
242
+ def main():
243
+ """Launch the Gradio interface."""
244
+ config = load_config()
245
+
246
+ # Extract default values
247
+ rhiz_defaults = config["species"][0]
248
+ avic_defaults = config["species"][1]
249
+ planting_defaults = config["project"]["planting_schedule"]
250
+ carbon_defaults = config["carbon"]
251
+ scenario_defaults = config.get("scenarios", {
252
+ "area": 1000.0,
253
+ "dbh_range": [1.0, 25.0],
254
+ "height_range": [0.5, 15.0],
255
+ "growth_rate_factor": 1.0
256
+ })
257
+
258
+ # Create interface
259
+ with gr.Blocks(title="ER Model Dashboard", theme=gr.themes.Soft()) as interface:
260
+ gr.Markdown("# ER Model Dashboard")
261
+ gr.Markdown("Explore carbon sequestration results and test different parameters")
262
+
263
+ with gr.Row():
264
+ with gr.Column():
265
+ # Rhizophora parameters
266
+ gr.Markdown("### Rhizophora Parameters")
267
+ rhiz_density = gr.Slider(
268
+ minimum=100,
269
+ maximum=10000,
270
+ value=rhiz_defaults["planting_density"],
271
+ label="Planting Density (trees/ha)"
272
+ )
273
+ rhiz_mort_1 = gr.Slider(0, 100, rhiz_defaults["mortality_rates"]["year_1"], label="Year 1 Mortality (%)")
274
+ rhiz_mort_2 = gr.Slider(0, 100, rhiz_defaults["mortality_rates"]["year_2"], label="Year 2 Mortality (%)")
275
+ rhiz_mort_3 = gr.Slider(0, 100, rhiz_defaults["mortality_rates"]["year_3"], label="Year 3 Mortality (%)")
276
+ rhiz_mort_4 = gr.Slider(0, 100, rhiz_defaults["mortality_rates"]["year_4"], label="Year 4 Mortality (%)")
277
+ rhiz_mort_5 = gr.Slider(0, 100, rhiz_defaults["mortality_rates"]["year_5"], label="Year 5 Mortality (%)")
278
+
279
+ with gr.Column():
280
+ # Avicennia parameters
281
+ gr.Markdown("### Avicennia Parameters")
282
+ avic_density = gr.Slider(
283
+ minimum=100,
284
+ maximum=10000,
285
+ value=avic_defaults["planting_density"],
286
+ label="Planting Density (trees/ha)"
287
+ )
288
+ avic_mort_1 = gr.Slider(0, 100, avic_defaults["mortality_rates"]["year_1"], label="Year 1 Mortality (%)")
289
+ avic_mort_2 = gr.Slider(0, 100, avic_defaults["mortality_rates"]["year_2"], label="Year 2 Mortality (%)")
290
+ avic_mort_3 = gr.Slider(0, 100, avic_defaults["mortality_rates"]["year_3"], label="Year 3 Mortality (%)")
291
+ avic_mort_4 = gr.Slider(0, 100, avic_defaults["mortality_rates"]["year_4"], label="Year 4 Mortality (%)")
292
+ avic_mort_5 = gr.Slider(0, 100, avic_defaults["mortality_rates"]["year_5"], label="Year 5 Mortality (%)")
293
+
294
+ with gr.Row():
295
+ with gr.Column():
296
+ # Planting schedule
297
+ gr.Markdown("### Planting Schedule")
298
+ year_1_area = gr.Slider(0, 1000, planting_defaults["year_1"], label="Year 1 Area (ha)")
299
+ year_2_area = gr.Slider(0, 1000, planting_defaults["year_2"], label="Year 2 Area (ha)")
300
+ year_3_area = gr.Slider(0, 1000, planting_defaults["year_3"], label="Year 3 Area (ha)")
301
+ year_4_area = gr.Slider(0, 1000, planting_defaults["year_4"], label="Year 4 Area (ha)")
302
+ year_5_area = gr.Slider(0, 1000, planting_defaults["year_5"], label="Year 5 Area (ha)")
303
+
304
+ with gr.Column():
305
+ # Carbon parameters
306
+ gr.Markdown("### Carbon Parameters")
307
+ buffer_percentage = gr.Slider(
308
+ minimum=0,
309
+ maximum=30,
310
+ value=carbon_defaults["buffer_percentage"],
311
+ label="Buffer Percentage (%)"
312
+ )
313
+
314
+ # Scenario parameters
315
+ gr.Markdown("### Scenario Parameters")
316
+ scenario_area = gr.Slider(
317
+ minimum=100,
318
+ maximum=2000,
319
+ value=scenario_defaults["area"],
320
+ label="Scenario Area (ha)"
321
+ )
322
+ dbh_range_min = gr.Slider(
323
+ minimum=0.5,
324
+ maximum=10,
325
+ value=scenario_defaults["dbh_range"][0],
326
+ label="Min DBH (cm)"
327
+ )
328
+ dbh_range_max = gr.Slider(
329
+ minimum=10,
330
+ maximum=30,
331
+ value=scenario_defaults["dbh_range"][1],
332
+ label="Max DBH (cm)"
333
+ )
334
+ height_range_min = gr.Slider(
335
+ minimum=0.5,
336
+ maximum=5,
337
+ value=scenario_defaults["height_range"][0],
338
+ label="Min Height (m)"
339
+ )
340
+ height_range_max = gr.Slider(
341
+ minimum=5,
342
+ maximum=20,
343
+ value=scenario_defaults["height_range"][1],
344
+ label="Max Height (m)"
345
+ )
346
+ growth_rate_factor = gr.Slider(
347
+ minimum=0.5,
348
+ maximum=2.0,
349
+ value=scenario_defaults["growth_rate_factor"],
350
+ label="Growth Rate Factor"
351
+ )
352
+
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")
360
+
361
+ summary = gr.Textbox(label="Summary", lines=10)
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",
370
+ headers=["Scenario", "DBH Range", "Height Range", "Growth Rate", "Year 5 tCO2", "Year 10 tCO2", "Year 15 tCO2", "Year 30 tCO2"]
371
+ )
372
+
373
+ # Update function
374
+ inputs = [
375
+ rhiz_density, rhiz_mort_1, rhiz_mort_2, rhiz_mort_3, rhiz_mort_4, rhiz_mort_5,
376
+ avic_density, avic_mort_1, avic_mort_2, avic_mort_3, avic_mort_4, avic_mort_5,
377
+ year_1_area, year_2_area, year_3_area, year_4_area, year_5_area,
378
+ buffer_percentage, scenario_area, dbh_range_min, dbh_range_max,
379
+ height_range_min, height_range_max, growth_rate_factor
380
+ ]
381
+
382
+ outputs = [plot1, plot2, plot3, summary, species_table, scenario_table]
383
+
384
+ # Initialize with default values
385
+ interface.load(
386
+ fn=update_model,
387
+ inputs=inputs,
388
+ outputs=outputs,
389
+ )
390
+
391
+ # Update on any change
392
+ for inp in inputs:
393
+ inp.change(
394
+ fn=update_model,
395
+ inputs=inputs,
396
+ outputs=outputs
397
+ )
398
+
399
+ # Launch interface
400
+ interface.launch(share=False)
401
+
402
+
403
+ if __name__ == "__main__":
404
+ main()
environment.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: er-model
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python=3.10
6
+ - numpy=1.24.3
7
+ - pandas=2.0.3
8
+ - scipy=1.12.0
9
+ - matplotlib=3.8.2
10
+ - pyyaml=6.0.1
11
+ - click=8.1.7
12
+ - pip=24.0
13
+ - pytest=7.4.4
14
+ - black=24.1.1
15
+ - isort=5.13.2
16
+ - pre-commit=3.6.0
17
+ - pip:
18
+ - gradio==3.50.2
19
+ - fastapi==0.95.2
20
+ - pydantic==1.10.13
scripts/run_pipeline.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ 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(
18
+ "--config",
19
+ type=click.Path(exists=True, path_type=Path),
20
+ required=True,
21
+ help="Path to YAML configuration file",
22
+ )
23
+ @click.option(
24
+ "--output",
25
+ type=click.Path(path_type=Path),
26
+ default=Path("outputs/results.csv"),
27
+ help="Path to save results CSV",
28
+ )
29
+ def main(config: Path, output: Path) -> None:
30
+ """Run the ER model pipeline."""
31
+ # Create output directory if it doesn't exist
32
+ output.parent.mkdir(parents=True, exist_ok=True)
33
+
34
+ # Initialize and run model
35
+ model = ERModel(config)
36
+ results, species_results, scenario_results = model.run()
37
+
38
+ # Save results
39
+ model.save_results(output)
40
+
41
+ # Save additional results
42
+ species_output = output.parent / "species_results.csv"
43
+ scenario_output = output.parent / "scenario_results.csv"
44
+ species_results.to_csv(species_output, index=False)
45
+ scenario_results.to_csv(scenario_output, index=False)
46
+
47
+ # Print summary
48
+ print("\nResults Summary:")
49
+ print("-" * 40)
50
+ print(f"Total years: {len(results)}")
51
+ print(f"Total gross carbon: {results['gross_carbon'].sum():.2f} tCO2")
52
+ print(f"Total net carbon: {results['net_carbon'].sum():.2f} tCO2")
53
+ print(f"\nResults saved to:")
54
+ print(f"- Main results: {output}")
55
+ print(f"- Species results: {species_output}")
56
+ print(f"- Scenario results: {scenario_output}")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
setup.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import find_packages, setup
2
+
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",
10
+ "scipy>=1.12.0",
11
+ "matplotlib>=3.8.2",
12
+ "pyyaml>=6.0.1",
13
+ "click>=8.1.7",
14
+ ],
15
+ python_requires=">=3.10",
16
+ )
src/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
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",
11
+ "Species",
12
+ "ProjectConfig",
13
+ "CarbonConfig",
14
+ "calculate_biomass",
15
+ "calculate_carbon",
16
+ ]
src/allometry.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Allometric equations for converting tree measurements to biomass.
3
+ """
4
+ from typing import Dict
5
+
6
+
7
+ def calculate_biomass(dbh: float, height: float, species_name: str, params: Dict[str, float]) -> float:
8
+ """
9
+ Calculate total tree biomass using species-specific allometric equations from Zanvo et al. 2023.
10
+
11
+ Args:
12
+ dbh: Diameter at breast height (cm)
13
+ height: Tree height (m)
14
+ species_name: Name of the species ("species_A" for Rhizophora, "species_B" for Avicennia)
15
+ params: Dictionary containing allometric parameters
16
+
17
+ Returns:
18
+ Total tree biomass (above + below ground) in kg
19
+ """
20
+ # Use Zanvo et al. 2023 equations that include both DBH and height
21
+ if species_name == "species_A": # Rhizophora
22
+ # Total = 1.938 × (DBH² H)^0.67628
23
+ total_biomass = 1.938 * (dbh**2 * height)**0.67628
24
+ elif species_name == "species_B": # Avicennia
25
+ # Total = 1.486 × (DBH² H)^0.55864
26
+ total_biomass = 1.486 * (dbh**2 * height)**0.55864
27
+ else:
28
+ # Use a conservative generic equation if species not recognized
29
+ total_biomass = 1.712 * (dbh**2 * height)**0.61746
30
+
31
+ return total_biomass
src/current_er_model.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
src/er_model.py ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core implementation of the Emissions Reduction (ER) model for mangrove projects.
3
+ """
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import yaml
11
+
12
+ from .allometry import calculate_biomass
13
+ from .metrics import calculate_carbon
14
+
15
+
16
+ @dataclass
17
+ class Species:
18
+ """Species-specific parameters for growth and carbon calculations."""
19
+ name: str
20
+ planting_density: float
21
+ mortality_rates: Dict[str, float]
22
+ chapman_richards: Dict[str, Dict[str, float]] # Nested dict for dbh and height parameters
23
+ allometry: Dict[str, float]
24
+ initial_values: Dict[str, float]
25
+
26
+
27
+ @dataclass
28
+ class ProjectConfig:
29
+ """Project configuration parameters."""
30
+ duration_years: int
31
+ planting_schedule: Dict[str, float]
32
+
33
+
34
+ @dataclass
35
+ class CarbonConfig:
36
+ """Carbon conversion and adjustment parameters."""
37
+ biomass_to_carbon: float
38
+ carbon_to_co2: float
39
+ buffer_percentage: float
40
+ leakage_percentage: float
41
+ baseline_emissions: float
42
+
43
+
44
+ class ERModel:
45
+ """
46
+ Emissions Reduction Model for mangrove projects.
47
+
48
+ Calculates carbon sequestration over time based on tree growth,
49
+ mortality, and carbon conversion factors.
50
+ """
51
+
52
+ def __init__(self, config_path: Path):
53
+ """
54
+ Initialize the model from a YAML configuration file.
55
+
56
+ Args:
57
+ config_path: Path to the YAML configuration file
58
+ """
59
+ with open(config_path) as f:
60
+ config = yaml.safe_load(f)
61
+
62
+ self.species = [Species(**s) for s in config["species"]]
63
+ self.project = ProjectConfig(**config["project"])
64
+ self.carbon = CarbonConfig(**config["carbon"])
65
+
66
+ # Store scenario parameters if provided
67
+ self.scenarios = config.get("scenarios", {
68
+ "area": 1000.0,
69
+ "dbh_range": [1.0, 20.0],
70
+ "height_range": [0.5, 12.0],
71
+ "growth_rate_factor": 1.0
72
+ })
73
+
74
+ # Initialize results storage
75
+ self.results: Optional[pd.DataFrame] = None
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.
104
+
105
+ Args:
106
+ age: Tree age in years
107
+ params: Dictionary with a, b, c parameters
108
+ initial_value: Initial value (DBH or height)
109
+
110
+ Returns:
111
+ Current size (DBH in cm or height in m)
112
+ """
113
+ a, b, c = params["a"], params["b"], params["c"]
114
+ return initial_value + (a - initial_value) * (1 - np.exp(-b * age)) ** c
115
+
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,
137
+ species.chapman_richards["dbh"],
138
+ species.initial_values["dbh"]
139
+ )
140
+ height = self.chapman_richards_growth(
141
+ age,
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],
159
+ growth_rate_factor: float) -> pd.Series:
160
+ """
161
+ Run a scenario with modified parameters.
162
+
163
+ Args:
164
+ area: Area to plant in hectares
165
+ dbh_range: [min_dbh, max_dbh] in cm
166
+ height_range: [min_height, max_height] in m
167
+ growth_rate_factor: Factor to multiply growth rate by
168
+
169
+ Returns:
170
+ Series with carbon sequestration at milestone years
171
+ """
172
+ # Create modified species for scenario
173
+ scenario_species = []
174
+ for sp in self.species:
175
+ mod_sp = Species(
176
+ name=sp.name,
177
+ planting_density=sp.planting_density,
178
+ mortality_rates=sp.mortality_rates.copy(),
179
+ chapman_richards={
180
+ "dbh": {
181
+ "a": np.mean(dbh_range),
182
+ "b": sp.chapman_richards["dbh"]["b"] * growth_rate_factor,
183
+ "c": sp.chapman_richards["dbh"]["c"]
184
+ },
185
+ "height": {
186
+ "a": np.mean(height_range),
187
+ "b": sp.chapman_richards["height"]["b"] * growth_rate_factor,
188
+ "c": sp.chapman_richards["height"]["c"]
189
+ }
190
+ },
191
+ allometry=sp.allometry.copy(),
192
+ initial_values=sp.initial_values.copy()
193
+ )
194
+ scenario_species.append(mod_sp)
195
+
196
+ # Calculate carbon at milestone years
197
+ milestones = [5, 10, 15, 30]
198
+ carbon_values = []
199
+
200
+ for year in milestones:
201
+ total_carbon = sum(
202
+ self.calculate_carbon_for_species(sp, year, area)
203
+ for sp in scenario_species
204
+ )
205
+ carbon_values.append(total_carbon)
206
+
207
+ return pd.Series(
208
+ carbon_values,
209
+ index=[f"Year {y} tCO2" for y in milestones]
210
+ )
211
+
212
+ def run(self) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
213
+ """
214
+ Execute the full ER calculation pipeline.
215
+
216
+ Returns:
217
+ Tuple of (yearly results DataFrame, species results DataFrame, scenario results DataFrame)
218
+ """
219
+ years = range(1, self.project.duration_years + 1)
220
+ results = []
221
+ species_results = []
222
+
223
+ for year in years:
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
248
+ species_year_results["Cumulative ha"] = cumulative_area
249
+ species_year_results["tCO2/ha"] = total_carbon / cumulative_area if cumulative_area > 0 else 0
250
+
251
+ # Apply adjustments for main results
252
+ gross_carbon = total_carbon
253
+ net_carbon = gross_carbon * (1 - self.carbon.buffer_percentage / 100)
254
+ net_carbon -= self.carbon.leakage_percentage / 100 * gross_carbon
255
+ net_carbon -= self.carbon.baseline_emissions * cumulative_area
256
+
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)
264
+ species_results.append(species_year_results)
265
+
266
+ self.results = pd.DataFrame(results)
267
+ self.species_results = pd.DataFrame(species_results)
268
+
269
+ # Run scenarios
270
+ scenarios = []
271
+ dbh_min, dbh_max = self.scenarios["dbh_range"]
272
+ height_min, height_max = self.scenarios["height_range"]
273
+ area = self.scenarios["area"]
274
+ growth_factor = self.scenarios["growth_rate_factor"]
275
+
276
+ # Base scenario
277
+ base_scenario = self.run_scenario(
278
+ area, [dbh_min, dbh_max], [height_min, height_max], growth_factor
279
+ )
280
+ scenarios.append({
281
+ "Scenario": "Base",
282
+ "DBH Range": f"{dbh_min:.1f}-{dbh_max:.1f}",
283
+ "Height Range": f"{height_min:.1f}-{height_max:.1f}",
284
+ "Growth Rate": f"{growth_factor:.1f}x",
285
+ **base_scenario
286
+ })
287
+
288
+ # Low growth scenario
289
+ low_scenario = self.run_scenario(
290
+ area,
291
+ [dbh_min * 0.5, dbh_max * 0.5],
292
+ [height_min * 0.5, height_max * 0.5],
293
+ growth_factor
294
+ )
295
+ scenarios.append({
296
+ "Scenario": "Low Growth",
297
+ "DBH Range": f"{dbh_min*0.5:.1f}-{dbh_max*0.5:.1f}",
298
+ "Height Range": f"{height_min*0.5:.1f}-{height_max*0.5:.1f}",
299
+ "Growth Rate": f"{growth_factor:.1f}x",
300
+ **low_scenario
301
+ })
302
+
303
+ # High growth scenario
304
+ high_scenario = self.run_scenario(
305
+ area,
306
+ [dbh_min * 1.5, dbh_max * 1.5],
307
+ [height_min * 1.5, height_max * 1.5],
308
+ growth_factor
309
+ )
310
+ scenarios.append({
311
+ "Scenario": "High Growth",
312
+ "DBH Range": f"{dbh_min*1.5:.1f}-{dbh_max*1.5:.1f}",
313
+ "Height Range": f"{height_min*1.5:.1f}-{height_max*1.5:.1f}",
314
+ "Growth Rate": f"{growth_factor:.1f}x",
315
+ **high_scenario
316
+ })
317
+
318
+ self.scenario_results = pd.DataFrame(scenarios)
319
+
320
+ return self.results, self.species_results, self.scenario_results
321
+
322
+ def save_results(self, output_path: Path) -> None:
323
+ """
324
+ Save results to CSV file.
325
+
326
+ Args:
327
+ output_path: Path to save the results CSV
328
+ """
329
+ if self.results is None:
330
+ raise ValueError("No results available. Run the model first.")
331
+ self.results.to_csv(output_path, index=False)
src/metrics.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Carbon sequestration metrics calculations.
3
+ """
4
+ from typing import Dict, List, Optional, Tuple
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+
10
+ def calculate_carbon(biomass: float, carbon_fraction: float, co2_conversion: float) -> float:
11
+ """
12
+ Convert biomass to carbon dioxide equivalent.
13
+
14
+ Args:
15
+ biomass: Total biomass in kg
16
+ carbon_fraction: Fraction of biomass that is carbon
17
+ co2_conversion: Conversion factor from C to CO2
18
+
19
+ Returns:
20
+ Carbon dioxide equivalent in tonnes
21
+ """
22
+ # Convert kg biomass to tonnes CO2
23
+ carbon = biomass * carbon_fraction # kg C
24
+ co2 = carbon * co2_conversion # kg CO2
25
+ tonnes_co2 = co2 / 1000.0 # tonnes CO2
26
+
27
+ return tonnes_co2
28
+
29
+
30
+ def calculate_milestone_metrics(
31
+ carbon_series: pd.Series,
32
+ milestones: List[int] = [5, 10, 15, 20, 25, 30]
33
+ ) -> Dict[str, float]:
34
+ """
35
+ Calculate carbon metrics at milestone years.
36
+
37
+ Args:
38
+ carbon_series: Series of carbon values indexed by year
39
+ milestones: List of milestone years to report
40
+
41
+ Returns:
42
+ Dictionary of milestone metrics
43
+ """
44
+ metrics = {}
45
+ for year in milestones:
46
+ if year in carbon_series.index:
47
+ metrics[f"Year_{year}"] = carbon_series[year]
48
+ return metrics
49
+
50
+
51
+ def calculate_per_hectare_metrics(
52
+ carbon: float,
53
+ area: float,
54
+ buffer_pct: float = 0.0,
55
+ leakage_pct: float = 0.0,
56
+ baseline: float = 0.0
57
+ ) -> Tuple[float, float]:
58
+ """
59
+ Calculate per-hectare carbon metrics with deductions.
60
+
61
+ Args:
62
+ carbon: Gross carbon in tCO2
63
+ area: Area in hectares
64
+ buffer_pct: Buffer pool percentage
65
+ leakage_pct: Leakage percentage
66
+ baseline: Baseline emissions in tCO2/ha/yr
67
+
68
+ Returns:
69
+ Tuple of (gross_per_ha, net_per_ha)
70
+ """
71
+ if area <= 0:
72
+ return 0.0, 0.0
73
+
74
+ gross_per_ha = carbon / area
75
+
76
+ # Apply deductions
77
+ net_carbon = carbon * (1 - buffer_pct/100) * (1 - leakage_pct/100)
78
+ net_carbon -= baseline * area
79
+ net_per_ha = max(0, net_carbon / area)
80
+
81
+ return gross_per_ha, net_per_ha
82
+
83
+
84
+ def calculate_scenario_metrics(
85
+ base_carbon: float,
86
+ scenario_carbon: float,
87
+ area: float
88
+ ) -> Dict[str, float]:
89
+ """
90
+ Calculate comparison metrics between base and scenario.
91
+
92
+ Args:
93
+ base_carbon: Base scenario carbon in tCO2
94
+ scenario_carbon: Alternative scenario carbon in tCO2
95
+ area: Area in hectares
96
+
97
+ Returns:
98
+ Dictionary of comparison metrics
99
+ """
100
+ metrics = {
101
+ "Absolute_Difference": scenario_carbon - base_carbon,
102
+ "Percent_Difference": ((scenario_carbon - base_carbon) / base_carbon * 100
103
+ if base_carbon > 0 else 0),
104
+ "Per_Hectare_Difference": (scenario_carbon - base_carbon) / area if area > 0 else 0
105
+ }
106
+ return metrics
tests/test_er_model.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for the ER model implementation.
3
+ """
4
+ from pathlib import Path
5
+
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
16
+ def sample_config():
17
+ """Create a minimal test configuration."""
18
+ return {
19
+ "species": [
20
+ {
21
+ "name": "Test Species",
22
+ "planting_density": 1000,
23
+ "mortality_rates": {
24
+ "year_1": 10,
25
+ "year_2": 5,
26
+ "subsequent": 2
27
+ },
28
+ "chapman_richards": {
29
+ "a": 30,
30
+ "b": 0.15,
31
+ "c": 1.5
32
+ },
33
+ "allometry": {
34
+ "biomass_equation": "0.2 * (dbh ** 2.4)",
35
+ "root_shoot_ratio": 0.4
36
+ }
37
+ }
38
+ ],
39
+ "project": {
40
+ "duration_years": 5,
41
+ "planting_schedule": {
42
+ "year_1": 100
43
+ }
44
+ },
45
+ "carbon": {
46
+ "biomass_to_carbon": 0.47,
47
+ "carbon_to_co2": 3.67,
48
+ "buffer_percentage": 15,
49
+ "leakage_percentage": 0,
50
+ "baseline_emissions": 0
51
+ }
52
+ }
53
+
54
+
55
+ @pytest.fixture
56
+ def config_file(tmp_path, sample_config):
57
+ """Create a temporary config file."""
58
+ config_path = tmp_path / "test_config.yaml"
59
+ with open(config_path, "w") as f:
60
+ yaml.dump(sample_config, f)
61
+ return config_path
62
+
63
+
64
+ def test_model_initialization(config_file):
65
+ """Test that the model initializes correctly from config."""
66
+ model = ERModel(config_file)
67
+ assert len(model.species) == 1
68
+ assert model.species[0].name == "Test Species"
69
+ assert model.project.duration_years == 5
70
+ assert model.carbon.biomass_to_carbon == 0.47
71
+
72
+
73
+ def test_surviving_trees_calculation(config_file):
74
+ """Test tree survival calculations."""
75
+ model = ERModel(config_file)
76
+ species = model.species[0]
77
+
78
+ # Test first year mortality
79
+ surviving = model.calculate_surviving_trees(1000, 1, species.mortality_rates)
80
+ assert surviving == 900 # 1000 * (1 - 0.10)
81
+
82
+ # Test cumulative mortality
83
+ surviving = model.calculate_surviving_trees(1000, 2, species.mortality_rates)
84
+ expected = 1000 * (1 - 0.10) * (1 - 0.05)
85
+ assert np.isclose(surviving, expected)
86
+
87
+
88
+ def test_chapman_richards_growth(config_file):
89
+ """Test DBH calculations using Chapman-Richards equation."""
90
+ model = ERModel(config_file)
91
+ species = model.species[0]
92
+
93
+ # Test initial growth
94
+ dbh = model.chapman_richards_dbh(0, species.chapman_richards)
95
+ assert dbh == 0
96
+
97
+ # Test asymptotic behavior
98
+ dbh = model.chapman_richards_dbh(100, species.chapman_richards)
99
+ assert np.isclose(dbh, species.chapman_richards["a"], rtol=0.01)
100
+
101
+
102
+ def test_biomass_calculation():
103
+ """Test biomass calculations from DBH."""
104
+ params = {
105
+ "biomass_equation": "0.2 * (dbh ** 2.4)",
106
+ "root_shoot_ratio": 0.4
107
+ }
108
+
109
+ biomass = calculate_biomass(10, params)
110
+ expected_agb = 0.2 * (10 ** 2.4)
111
+ expected_total = expected_agb * (1 + 0.4)
112
+ assert np.isclose(biomass, expected_total)
113
+
114
+
115
+ def test_carbon_calculation():
116
+ """Test carbon conversion calculations."""
117
+ biomass = 1000 # kg
118
+ biomass_to_carbon = 0.47
119
+ carbon_to_co2 = 3.67
120
+
121
+ co2 = calculate_carbon(biomass, biomass_to_carbon, carbon_to_co2)
122
+ expected = (1000 * 0.47 * 3.67) / 1000 # Convert to metric tons
123
+ assert np.isclose(co2, expected)
124
+
125
+
126
+ def test_full_pipeline(config_file):
127
+ """Test the complete model pipeline."""
128
+ model = ERModel(config_file)
129
+ results = model.run()
130
+
131
+ assert len(results) == 5 # Duration years
132
+ assert "year" in results.columns
133
+ assert "gross_carbon" in results.columns
134
+ assert "net_carbon" in results.columns
135
+ assert all(results["net_carbon"] <= results["gross_carbon"])