malcolmSQ commited on
Commit ·
fe152d4
0
Parent(s):
Initial commit: ER model for Nigeria mangroves
Browse files- .cursor/rules/activate_env.yml +9 -0
- .cursor/rules/code_quality.yml +29 -0
- .cursor/rules/er_model_guidelines.md +69 -0
- .gitignore +42 -0
- .pre-commit-config.yaml +27 -0
- README.md +153 -0
- configs/params.yaml +76 -0
- dashboard/app.py +404 -0
- environment.yml +20 -0
- scripts/run_pipeline.py +60 -0
- setup.py +16 -0
- src/__init__.py +16 -0
- src/allometry.py +31 -0
- src/current_er_model.ipynb +0 -0
- src/er_model.py +331 -0
- src/metrics.py +106 -0
- tests/test_er_model.py +135 -0
.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"])
|