malcolmSQ commited on
Commit ·
0cefa06
1
Parent(s): 0dba6f7
Refactor declining_increment_growth to accumulate increments, fix negative height/DBH; enable robust multi-model support
Browse files- README.md +79 -111
- configs/declining_increment.yaml +78 -0
- configs/linear.yaml +74 -0
- configs/linear_plateau.yaml +82 -0
- configs/params.yaml +7 -7
- dashboard/app.py +383 -434
- models/original/README.md +30 -0
- src/allometry.py +3 -0
- src/current_er_model (1).ipynb +0 -0
- src/er_model.py +168 -166
- tests/test_er_model.py +54 -20
README.md
CHANGED
|
@@ -1,124 +1,92 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
A Python
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
##
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
- Height growth rate (b): Changed from 0.1237 to 0.1335 yr⁻¹
|
| 11 |
-
- Height shape parameter (c): Changed from 2.1 to 1.5
|
| 12 |
-
- Rhizophora height asymptote: Changed from 12.0m to 11.58m
|
| 13 |
-
|
| 14 |
-
- **Allometric Equations**: Using Zanvo et al. (2023) species-specific equations:
|
| 15 |
-
- Rhizophora: Total = 1.938 × (DBH² H)^0.67628
|
| 16 |
-
- Avicennia: Total = 1.486 × (DBH² H)^0.55864
|
| 17 |
-
|
| 18 |
-
### Dashboard Improvements
|
| 19 |
-
- **Enhanced Parameter Controls**:
|
| 20 |
-
- Direct input of Chapman-Richards parameters
|
| 21 |
-
- Separate growth parameters for DBH and height
|
| 22 |
-
- Real-time model updates
|
| 23 |
-
- Improved validation and error handling
|
| 24 |
-
|
| 25 |
-
- **Improved Visualizations**:
|
| 26 |
-
- Carbon sequestration over time
|
| 27 |
-
- Annual emission reductions
|
| 28 |
-
- Per-tree biomass growth curves
|
| 29 |
-
- Enhanced data tables and summaries
|
| 30 |
-
|
| 31 |
-
- **Detailed Metrics**:
|
| 32 |
-
- Project overview with total area and buffer pool
|
| 33 |
-
- Comprehensive carbon sequestration metrics
|
| 34 |
-
- Milestone year comparisons (10, 20, 30 years)
|
| 35 |
-
- Per hectare metrics with annual rates
|
| 36 |
-
- Species-specific results
|
| 37 |
-
|
| 38 |
-
## Model Parameters
|
| 39 |
-
|
| 40 |
-
### Species Parameters
|
| 41 |
-
- **Rhizophora spp.**
|
| 42 |
-
- DBH: a = 11.07 cm, b = 0.4736 yr⁻¹, c = 2.1
|
| 43 |
-
- Height: a = 11.58 m, b = 0.1335 yr⁻¹, c = 1.5
|
| 44 |
-
- Initial values: DBH = 1.0 cm, Height = 0.5 m
|
| 45 |
-
- Configurable planting density and mortality rates
|
| 46 |
-
|
| 47 |
-
- **Avicennia germinans**
|
| 48 |
-
- DBH: a = 11.07 cm, b = 0.4736 yr⁻¹, c = 2.1
|
| 49 |
-
- Height: a = 11.58 m, b = 0.1335 yr⁻¹, c = 1.5
|
| 50 |
-
- Initial values: DBH = 1.0 cm, Height = 0.5 m
|
| 51 |
-
- Configurable planting density and mortality rates
|
| 52 |
-
|
| 53 |
-
### Project Configuration
|
| 54 |
-
- Total area: 5000 ha (2500 ha each in years 1-2)
|
| 55 |
-
- Project duration: 30 years
|
| 56 |
-
- Buffer pool: 20%
|
| 57 |
-
- Species mix: Rhizophora spp. and Avicennia germinans
|
| 58 |
-
|
| 59 |
-
## Installation
|
| 60 |
-
|
| 61 |
-
1. Install [Miniconda](https://docs.conda.io/en/latest/miniconda.html) or [Miniforge](https://github.com/conda-forge/miniforge)
|
| 62 |
-
|
| 63 |
-
2. Create and activate the environment:
|
| 64 |
```bash
|
| 65 |
conda env create -f environment.yml
|
| 66 |
conda activate er-model
|
| 67 |
-
```
|
| 68 |
-
|
| 69 |
-
3. Install pre-commit hooks:
|
| 70 |
-
```bash
|
| 71 |
pre-commit install
|
| 72 |
```
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
1. Configure parameters in `configs/params.yaml`
|
| 77 |
-
|
| 78 |
-
2. Launch the dashboard:
|
| 79 |
```bash
|
| 80 |
python -m dashboard.app
|
| 81 |
```
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
##
|
| 96 |
-
|
| 97 |
-
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
#
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
- Pre-commit hooks ensure code quality
|
| 121 |
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
|
|
|
| 124 |
[License details to be added]
|
|
|
|
| 1 |
+
# Mangrove Emissions Reduction (ER) Modeling Suite
|
| 2 |
+
|
| 3 |
+
A Python toolkit and interactive dashboard for modeling carbon sequestration and tree survival in mangrove restoration projects. Supports multiple growth and mortality models, including Chapman-Richards and Jimenez & Lugo parameterizations.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🚀 Quickstart
|
| 8 |
+
|
| 9 |
+
1. **Install dependencies**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
```bash
|
| 11 |
conda env create -f environment.yml
|
| 12 |
conda activate er-model
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
pre-commit install
|
| 14 |
```
|
| 15 |
|
| 16 |
+
2. **Launch the dashboard**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
```bash
|
| 18 |
python -m dashboard.app
|
| 19 |
```
|
| 20 |
+
- The dashboard opens in your browser.
|
| 21 |
+
- Each tab represents a different model/configuration.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## 🖥️ Dashboard Usage
|
| 26 |
+
- **Multi-model:** Switch between models (e.g., "Original Model", "Jimenez & Lugo") using tabs.
|
| 27 |
+
- **Interactive plots:** All plots are interactive (hover, zoom, export).
|
| 28 |
+
- **Parameter editing:** Adjust planting schedule and see real-time results (where enabled).
|
| 29 |
+
- **Tables:** View carbon, species, and survival tables for each model.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 📚 Model Documentation
|
| 34 |
+
|
| 35 |
+
- [Original Model](models/original/README.md): Chapman-Richards growth, DBH-dependent mortality, field-calibrated.
|
| 36 |
+
- [Jimenez & Lugo Model](models/jimenez_lugo/README.md): Literature-based, fixed annual mortality rates.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## 🗂️ Directory Structure
|
| 41 |
+
```
|
| 42 |
+
er-chapman-richards/
|
| 43 |
+
├─�� README.md # This file
|
| 44 |
+
├── environment.yml # Conda environment
|
| 45 |
+
├── dashboard/ # Gradio dashboard app
|
| 46 |
+
├── src/ # Core model code
|
| 47 |
+
├── configs/ # Model parameter YAMLs
|
| 48 |
+
├── models/ # Model-specific docs
|
| 49 |
+
│ ├── original/README.md
|
| 50 |
+
│ └── jimenez_lugo/README.md
|
| 51 |
+
├── tests/ # Pytest suite
|
| 52 |
+
└── ...
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## ➕ Adding a New Model
|
| 58 |
+
1. Create a new config YAML in `configs/` (e.g., `configs/my_model.yaml`).
|
| 59 |
+
2. Add a new entry to `MODEL_CONFIGS` in `dashboard/app.py`:
|
| 60 |
+
```python
|
| 61 |
+
MODEL_CONFIGS = {
|
| 62 |
+
"Original Model": "configs/params.yaml",
|
| 63 |
+
"Jimenez & Lugo": "configs/jimenez_lugo.yaml",
|
| 64 |
+
"My Model": "configs/my_model.yaml"
|
| 65 |
+
}
|
| 66 |
+
```
|
| 67 |
+
3. (Optional) Add a new subdirectory and README in `models/` for documentation.
|
| 68 |
+
4. Restart the dashboard. Your model appears as a new tab.
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## 🧑💻 Development & Testing
|
| 73 |
+
- Code style: [black](https://black.readthedocs.io/), [isort](https://pycqa.github.io/isort/)
|
| 74 |
+
- Type hints required for all public functions
|
| 75 |
+
- Run tests with:
|
| 76 |
+
```bash
|
| 77 |
+
pytest
|
| 78 |
+
```
|
| 79 |
- Pre-commit hooks ensure code quality
|
| 80 |
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 📖 References
|
| 84 |
+
- Chapman-Richards growth model literature
|
| 85 |
+
- Jimenez, J.A. & Lugo, A.E. (1985, 1987)
|
| 86 |
+
- Zanvo et al. (2023) for allometric equations
|
| 87 |
+
- Field data from Nigeria mangrove sites (2024)
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
|
| 91 |
+
## 📝 License
|
| 92 |
[License details to be added]
|
configs/declining_increment.yaml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
growth_model: declining_increment
|
| 2 |
+
|
| 3 |
+
species:
|
| 4 |
+
- name: "species_A" # Rhizophora spp.
|
| 5 |
+
planting_density: 4000 # trees/ha
|
| 6 |
+
mortality_rates:
|
| 7 |
+
year_1: 15 # %
|
| 8 |
+
year_2: 12 # %
|
| 9 |
+
year_3: 6 # %
|
| 10 |
+
year_4: 5 # %
|
| 11 |
+
year_5: 5 # %
|
| 12 |
+
year_6: 5 # %
|
| 13 |
+
year_7: 5 # %
|
| 14 |
+
year_8: 5 # %
|
| 15 |
+
year_9: 5 # %
|
| 16 |
+
year_10: 5 # %
|
| 17 |
+
subsequent: 1.5 # % (years 11-30)
|
| 18 |
+
declining_increment:
|
| 19 |
+
dbh:
|
| 20 |
+
r0: 1.83 # cm/yr
|
| 21 |
+
T_m: 30 # years
|
| 22 |
+
height:
|
| 23 |
+
r0: 0.61 # m/yr
|
| 24 |
+
T_m: 12 # years
|
| 25 |
+
allometry:
|
| 26 |
+
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
| 27 |
+
initial_values:
|
| 28 |
+
dbh: 1.0 # cm
|
| 29 |
+
height: 0.5 # m
|
| 30 |
+
|
| 31 |
+
- name: "species_B" # Avicennia germinans
|
| 32 |
+
planting_density: 444 # trees/ha
|
| 33 |
+
mortality_rates:
|
| 34 |
+
year_1: 15 # %
|
| 35 |
+
year_2: 12 # %
|
| 36 |
+
year_3: 6 # %
|
| 37 |
+
year_4: 5 # %
|
| 38 |
+
year_5: 5 # %
|
| 39 |
+
year_6: 5 # %
|
| 40 |
+
year_7: 5 # %
|
| 41 |
+
year_8: 5 # %
|
| 42 |
+
year_9: 5 # %
|
| 43 |
+
year_10: 5 # %
|
| 44 |
+
subsequent: 1.5 # % (years 11-30)
|
| 45 |
+
declining_increment:
|
| 46 |
+
dbh:
|
| 47 |
+
r0: 1.2 # cm/yr (example, adjust as needed)
|
| 48 |
+
T_m: 30
|
| 49 |
+
height:
|
| 50 |
+
r0: 0.4 # m/yr (example, adjust as needed)
|
| 51 |
+
T_m: 12
|
| 52 |
+
allometry:
|
| 53 |
+
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
| 54 |
+
initial_values:
|
| 55 |
+
dbh: 1.0 # cm
|
| 56 |
+
height: 0.5 # m
|
| 57 |
+
|
| 58 |
+
project:
|
| 59 |
+
duration_years: 30 # matches project_years
|
| 60 |
+
planting_schedule:
|
| 61 |
+
year_1: 2500 # ha
|
| 62 |
+
year_2: 2500 # ha
|
| 63 |
+
year_3: 0 # ha
|
| 64 |
+
year_4: 0 # ha
|
| 65 |
+
year_5: 0 # ha
|
| 66 |
+
|
| 67 |
+
carbon:
|
| 68 |
+
biomass_to_carbon: 0.47 # IPCC default
|
| 69 |
+
carbon_to_co2: 3.67 # molecular weight ratio
|
| 70 |
+
buffer_percentage: 20 # risk buffer
|
| 71 |
+
leakage_percentage: 0 # no leakage assumed
|
| 72 |
+
baseline_emissions: 0 # tCO2/ha/year
|
| 73 |
+
|
| 74 |
+
scenarios:
|
| 75 |
+
area: 1000.0 # ha
|
| 76 |
+
dbh_range: [1.0, 25.0] # cm
|
| 77 |
+
height_range: [0.5, 15.0] # m
|
| 78 |
+
growth_rate_factor: 1.0
|
configs/linear.yaml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
growth_model: linear
|
| 2 |
+
|
| 3 |
+
species:
|
| 4 |
+
- name: "species_A" # Rhizophora spp.
|
| 5 |
+
planting_density: 4000 # trees/ha
|
| 6 |
+
mortality_rates:
|
| 7 |
+
year_1: 15 # %
|
| 8 |
+
year_2: 12 # %
|
| 9 |
+
year_3: 6 # %
|
| 10 |
+
year_4: 5 # %
|
| 11 |
+
year_5: 5 # %
|
| 12 |
+
year_6: 5 # %
|
| 13 |
+
year_7: 5 # %
|
| 14 |
+
year_8: 5 # %
|
| 15 |
+
year_9: 5 # %
|
| 16 |
+
year_10: 5 # %
|
| 17 |
+
subsequent: 1.5 # % (years 11-30)
|
| 18 |
+
linear:
|
| 19 |
+
dbh:
|
| 20 |
+
r: 1.83 # cm/yr
|
| 21 |
+
height:
|
| 22 |
+
r: 0.61 # m/yr
|
| 23 |
+
allometry:
|
| 24 |
+
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
| 25 |
+
initial_values:
|
| 26 |
+
dbh: 1.0 # cm
|
| 27 |
+
height: 0.5 # m
|
| 28 |
+
|
| 29 |
+
- name: "species_B" # Avicennia germinans
|
| 30 |
+
planting_density: 444 # trees/ha
|
| 31 |
+
mortality_rates:
|
| 32 |
+
year_1: 15 # %
|
| 33 |
+
year_2: 12 # %
|
| 34 |
+
year_3: 6 # %
|
| 35 |
+
year_4: 5 # %
|
| 36 |
+
year_5: 5 # %
|
| 37 |
+
year_6: 5 # %
|
| 38 |
+
year_7: 5 # %
|
| 39 |
+
year_8: 5 # %
|
| 40 |
+
year_9: 5 # %
|
| 41 |
+
year_10: 5 # %
|
| 42 |
+
subsequent: 1.5 # % (years 11-30)
|
| 43 |
+
linear:
|
| 44 |
+
dbh:
|
| 45 |
+
r: 1.2 # cm/yr (example, adjust as needed)
|
| 46 |
+
height:
|
| 47 |
+
r: 0.4 # m/yr (example, adjust as needed)
|
| 48 |
+
allometry:
|
| 49 |
+
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
| 50 |
+
initial_values:
|
| 51 |
+
dbh: 1.0 # cm
|
| 52 |
+
height: 0.5 # m
|
| 53 |
+
|
| 54 |
+
project:
|
| 55 |
+
duration_years: 30 # matches project_years
|
| 56 |
+
planting_schedule:
|
| 57 |
+
year_1: 2500 # ha
|
| 58 |
+
year_2: 2500 # ha
|
| 59 |
+
year_3: 0 # ha
|
| 60 |
+
year_4: 0 # ha
|
| 61 |
+
year_5: 0 # ha
|
| 62 |
+
|
| 63 |
+
carbon:
|
| 64 |
+
biomass_to_carbon: 0.47 # IPCC default
|
| 65 |
+
carbon_to_co2: 3.67 # molecular weight ratio
|
| 66 |
+
buffer_percentage: 20 # risk buffer
|
| 67 |
+
leakage_percentage: 0 # no leakage assumed
|
| 68 |
+
baseline_emissions: 0 # tCO2/ha/year
|
| 69 |
+
|
| 70 |
+
scenarios:
|
| 71 |
+
area: 1000.0 # ha
|
| 72 |
+
dbh_range: [1.0, 25.0] # cm
|
| 73 |
+
height_range: [0.5, 15.0] # m
|
| 74 |
+
growth_rate_factor: 1.0
|
configs/linear_plateau.yaml
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
growth_model: linear_plateau
|
| 2 |
+
|
| 3 |
+
species:
|
| 4 |
+
- name: "species_A" # Rhizophora spp.
|
| 5 |
+
planting_density: 4000 # trees/ha
|
| 6 |
+
mortality_rates:
|
| 7 |
+
year_1: 15 # %
|
| 8 |
+
year_2: 12 # %
|
| 9 |
+
year_3: 6 # %
|
| 10 |
+
year_4: 5 # %
|
| 11 |
+
year_5: 5 # %
|
| 12 |
+
year_6: 5 # %
|
| 13 |
+
year_7: 5 # %
|
| 14 |
+
year_8: 5 # %
|
| 15 |
+
year_9: 5 # %
|
| 16 |
+
year_10: 5 # %
|
| 17 |
+
subsequent: 1.5 # % (years 11-30)
|
| 18 |
+
linear_plateau:
|
| 19 |
+
dbh:
|
| 20 |
+
r: 1.83 # cm/yr
|
| 21 |
+
T_p: 30 # plateau at 30 years
|
| 22 |
+
a: 11.07 # asymptote from params.yaml
|
| 23 |
+
height:
|
| 24 |
+
r: 0.61 # m/yr
|
| 25 |
+
T_p: 12 # plateau at 12 years
|
| 26 |
+
a: 11.58 # asymptote from params.yaml
|
| 27 |
+
allometry:
|
| 28 |
+
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
| 29 |
+
initial_values:
|
| 30 |
+
dbh: 1.0 # cm
|
| 31 |
+
height: 0.5 # m
|
| 32 |
+
|
| 33 |
+
- name: "species_B" # Avicennia germinans
|
| 34 |
+
planting_density: 444 # trees/ha
|
| 35 |
+
mortality_rates:
|
| 36 |
+
year_1: 15 # %
|
| 37 |
+
year_2: 12 # %
|
| 38 |
+
year_3: 6 # %
|
| 39 |
+
year_4: 5 # %
|
| 40 |
+
year_5: 5 # %
|
| 41 |
+
year_6: 5 # %
|
| 42 |
+
year_7: 5 # %
|
| 43 |
+
year_8: 5 # %
|
| 44 |
+
year_9: 5 # %
|
| 45 |
+
year_10: 5 # %
|
| 46 |
+
subsequent: 1.5 # % (years 11-30)
|
| 47 |
+
linear_plateau:
|
| 48 |
+
dbh:
|
| 49 |
+
r: 1.2 # cm/yr (example, adjust as needed)
|
| 50 |
+
T_p: 30
|
| 51 |
+
a: 17.0
|
| 52 |
+
height:
|
| 53 |
+
r: 0.4 # m/yr (example, adjust as needed)
|
| 54 |
+
T_p: 12
|
| 55 |
+
a: 8.8
|
| 56 |
+
allometry:
|
| 57 |
+
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
| 58 |
+
initial_values:
|
| 59 |
+
dbh: 1.0 # cm
|
| 60 |
+
height: 0.5 # m
|
| 61 |
+
|
| 62 |
+
project:
|
| 63 |
+
duration_years: 30 # matches project_years
|
| 64 |
+
planting_schedule:
|
| 65 |
+
year_1: 2500 # ha
|
| 66 |
+
year_2: 2500 # ha
|
| 67 |
+
year_3: 0 # ha
|
| 68 |
+
year_4: 0 # ha
|
| 69 |
+
year_5: 0 # ha
|
| 70 |
+
|
| 71 |
+
carbon:
|
| 72 |
+
biomass_to_carbon: 0.47 # IPCC default
|
| 73 |
+
carbon_to_co2: 3.67 # molecular weight ratio
|
| 74 |
+
buffer_percentage: 20 # risk buffer
|
| 75 |
+
leakage_percentage: 0 # no leakage assumed
|
| 76 |
+
baseline_emissions: 0 # tCO2/ha/year
|
| 77 |
+
|
| 78 |
+
scenarios:
|
| 79 |
+
area: 1000.0 # ha
|
| 80 |
+
dbh_range: [1.0, 25.0] # cm
|
| 81 |
+
height_range: [0.5, 15.0] # m
|
| 82 |
+
growth_rate_factor: 1.0
|
configs/params.yaml
CHANGED
|
@@ -16,12 +16,12 @@ species:
|
|
| 16 |
chapman_richards:
|
| 17 |
dbh:
|
| 18 |
a: 11.07 # asymptotic maximum DBH (cm)
|
| 19 |
-
b: 0.
|
| 20 |
c: 2.1 # shape parameter
|
| 21 |
height:
|
| 22 |
-
a:
|
| 23 |
-
b: 0.
|
| 24 |
-
c:
|
| 25 |
allometry:
|
| 26 |
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
| 27 |
initial_values:
|
|
@@ -45,12 +45,12 @@ species:
|
|
| 45 |
chapman_richards:
|
| 46 |
dbh:
|
| 47 |
a: 17.0 # asymptotic maximum DBH (cm)
|
| 48 |
-
b: 0.
|
| 49 |
c: 2.1 # shape parameter
|
| 50 |
height:
|
| 51 |
a: 8.8 # asymptotic maximum height (m)
|
| 52 |
-
b: 0.
|
| 53 |
-
c:
|
| 54 |
allometry:
|
| 55 |
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
| 56 |
initial_values:
|
|
|
|
| 16 |
chapman_richards:
|
| 17 |
dbh:
|
| 18 |
a: 11.07 # asymptotic maximum DBH (cm)
|
| 19 |
+
b: 0.4736 # growth rate parameter (yr^-1), back-solved from Aug 2024 data
|
| 20 |
c: 2.1 # shape parameter
|
| 21 |
height:
|
| 22 |
+
a: 11.58 # asymptotic maximum height (m)
|
| 23 |
+
b: 0.1335 # growth rate parameter (yr^-1), back-solved from Aug 2024 data
|
| 24 |
+
c: 1.5 # shape parameter
|
| 25 |
allometry:
|
| 26 |
equation: "Zanvo et al. 2023: Total = 1.938 × (DBH² H)^0.67628"
|
| 27 |
initial_values:
|
|
|
|
| 45 |
chapman_richards:
|
| 46 |
dbh:
|
| 47 |
a: 17.0 # asymptotic maximum DBH (cm)
|
| 48 |
+
b: 0.4736 # growth rate parameter (yr^-1), back-solved from Aug 2024 data
|
| 49 |
c: 2.1 # shape parameter
|
| 50 |
height:
|
| 51 |
a: 8.8 # asymptotic maximum height (m)
|
| 52 |
+
b: 0.1335 # growth rate parameter (yr^-1), back-solved from Aug 2024 data
|
| 53 |
+
c: 1.5 # shape parameter
|
| 54 |
allometry:
|
| 55 |
equation: "Zanvo et al. 2023: Total = 1.486 × (DBH² H)^0.55864"
|
| 56 |
initial_values:
|
dashboard/app.py
CHANGED
|
@@ -1,459 +1,408 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 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
|
| 12 |
-
import
|
| 13 |
-
|
| 14 |
-
|
|
|
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
with open(config_path) as f:
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
def
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
def
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
if isinstance(x, int) or float(x).is_integer():
|
| 50 |
-
return format(int(x), ',')
|
| 51 |
-
return format(round(float(x), 2), ',')
|
| 52 |
-
return str(x)
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
def
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
results.plot(
|
| 74 |
-
x="year",
|
| 75 |
-
y=["gross_carbon", "net_carbon"],
|
| 76 |
-
ax=ax1,
|
| 77 |
-
title="Carbon Sequestration Over Time"
|
| 78 |
-
)
|
| 79 |
-
ax1.set_xlabel("Year")
|
| 80 |
-
ax1.set_ylabel("Carbon (tCO2)")
|
| 81 |
-
ax1.legend(["Gross Carbon", "Net Carbon"])
|
| 82 |
-
# Format y-axis with comma separator
|
| 83 |
-
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 84 |
-
|
| 85 |
-
# Annual ERs plot (replacing milestone years plot)
|
| 86 |
-
fig2 = plt.figure(figsize=(10, 6))
|
| 87 |
-
ax2 = fig2.add_subplot(111)
|
| 88 |
-
|
| 89 |
-
# Calculate annual ERs (year-over-year change in net carbon)
|
| 90 |
-
annual_ers = results["net_carbon"].diff()
|
| 91 |
-
annual_ers.iloc[0] = results["net_carbon"].iloc[0] # First year is total, not difference
|
| 92 |
-
|
| 93 |
-
# Plot annual ERs as bars
|
| 94 |
-
ax2.bar(results["year"], annual_ers, color='#2ecc71', alpha=0.7)
|
| 95 |
-
ax2.set_xlabel("Year")
|
| 96 |
-
ax2.set_ylabel("Annual ERs (tCO2)")
|
| 97 |
-
ax2.set_title("Annual Emission Reductions")
|
| 98 |
-
|
| 99 |
-
# Format y-axis with comma separator
|
| 100 |
-
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 101 |
-
|
| 102 |
-
# Add grid for better readability
|
| 103 |
-
ax2.grid(True, axis='y', linestyle='--', alpha=0.7)
|
| 104 |
-
|
| 105 |
-
# Ensure integer ticks for years
|
| 106 |
-
ax2.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
|
| 107 |
|
| 108 |
-
#
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
ax3.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: format_number(x)))
|
| 152 |
-
|
| 153 |
-
# Ensure the plot is drawn
|
| 154 |
-
fig3.tight_layout()
|
| 155 |
-
plt.draw()
|
| 156 |
|
| 157 |
-
|
| 158 |
-
total_area = sum([
|
| 159 |
-
year_1_area, year_2_area, year_3_area, year_4_area, year_5_area
|
| 160 |
-
])
|
| 161 |
-
|
| 162 |
-
# Get final year carbon values
|
| 163 |
final_gross = results["gross_carbon"].iloc[-1]
|
| 164 |
final_net = results["net_carbon"].iloc[-1]
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
if total_area > 0
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
-------------------
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
Milestone Years:
|
| 207 |
-
--------------
|
| 208 |
-
Year 10 Gross: {format_number(year_10_gross)} tCO2
|
| 209 |
-
Year 10 Net: {format_number(year_10_net)} tCO2
|
| 210 |
-
Year 20 Gross: {format_number(year_20_gross)} tCO2
|
| 211 |
-
Year 20 Net: {format_number(year_20_net)} tCO2
|
| 212 |
-
|
| 213 |
-
Per Hectare Metrics (Year 30):
|
| 214 |
-
-----------------------------
|
| 215 |
-
Gross Carbon per ha: {format_number(gross_carbon_per_ha)} tCO2/ha
|
| 216 |
-
Net Carbon per ha: {format_number(net_carbon_per_ha)} tCO2/ha
|
| 217 |
-
Average Annual Net per ha: {format_number(annual_net_per_ha)} tCO2/ha/yr
|
| 218 |
-
"""
|
| 219 |
-
|
| 220 |
-
# Format species results table with comma separators
|
| 221 |
-
species_table = species_results.copy()
|
| 222 |
-
species_table.index.name = "Year"
|
| 223 |
-
for col in species_table.select_dtypes(include=[np.number]).columns:
|
| 224 |
-
species_table[col] = species_table[col].apply(format_number)
|
| 225 |
-
|
| 226 |
-
# Create annual ERs table
|
| 227 |
-
annual_results = pd.DataFrame({
|
| 228 |
-
'Year': results['year'],
|
| 229 |
-
'Annual ERs (tCO2)': annual_ers,
|
| 230 |
-
'Cumulative ERs (tCO2)': results['net_carbon'],
|
| 231 |
-
'Area Planted (ha)': results[[col for col in results.columns if col.startswith('area_year_')]].sum(axis=1),
|
| 232 |
-
'Cumulative Area (ha)': results[[col for col in results.columns if col.startswith('area_year_')]].cumsum().sum(axis=1)
|
| 233 |
-
})
|
| 234 |
-
|
| 235 |
-
# Format annual results table with comma separators
|
| 236 |
-
for col in annual_results.select_dtypes(include=[np.number]).columns:
|
| 237 |
-
annual_results[col] = annual_results[col].apply(format_number)
|
| 238 |
-
|
| 239 |
-
# Format scenario comparison table with comma separators
|
| 240 |
-
scenario_table = scenario_results.copy()
|
| 241 |
-
scenario_table.index.name = "Scenario"
|
| 242 |
-
for col in scenario_table.select_dtypes(include=[np.number]).columns:
|
| 243 |
-
scenario_table[col] = scenario_table[col].apply(format_number)
|
| 244 |
-
|
| 245 |
-
return fig1, fig2, fig3, summary, annual_results, scenario_table
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
def update_model(
|
| 249 |
-
rhiz_density: float,
|
| 250 |
-
rhiz_mort_1: float,
|
| 251 |
-
rhiz_mort_2: float,
|
| 252 |
-
rhiz_mort_3: float,
|
| 253 |
-
rhiz_mort_4: float,
|
| 254 |
-
rhiz_mort_5: float,
|
| 255 |
-
avic_density: float,
|
| 256 |
-
avic_mort_1: float,
|
| 257 |
-
avic_mort_2: float,
|
| 258 |
-
avic_mort_3: float,
|
| 259 |
-
avic_mort_4: float,
|
| 260 |
-
avic_mort_5: float,
|
| 261 |
-
year_1_area: float,
|
| 262 |
-
year_2_area: float,
|
| 263 |
-
year_3_area: float,
|
| 264 |
-
year_4_area: float,
|
| 265 |
-
year_5_area: float,
|
| 266 |
-
buffer_percentage: float,
|
| 267 |
-
# Chapman-Richards parameters for Rhizophora
|
| 268 |
-
rhiz_dbh_a: float,
|
| 269 |
-
rhiz_dbh_b: float,
|
| 270 |
-
rhiz_dbh_c: float,
|
| 271 |
-
rhiz_height_a: float,
|
| 272 |
-
rhiz_height_b: float,
|
| 273 |
-
rhiz_height_c: float,
|
| 274 |
-
rhiz_initial_dbh: float,
|
| 275 |
-
rhiz_initial_height: float,
|
| 276 |
-
# Chapman-Richards parameters for Avicennia
|
| 277 |
-
avic_dbh_a: float,
|
| 278 |
-
avic_dbh_b: float,
|
| 279 |
-
avic_dbh_c: float,
|
| 280 |
-
avic_height_a: float,
|
| 281 |
-
avic_height_b: float,
|
| 282 |
-
avic_height_c: float,
|
| 283 |
-
avic_initial_dbh: float,
|
| 284 |
-
avic_initial_height: float,
|
| 285 |
-
) -> Tuple[plt.Figure, plt.Figure, plt.Figure, str, pd.DataFrame, pd.DataFrame]:
|
| 286 |
-
"""Update model with new parameters and return plots and tables."""
|
| 287 |
-
# Load base config
|
| 288 |
-
config = load_config()
|
| 289 |
-
|
| 290 |
-
# Update Rhizophora parameters
|
| 291 |
-
config["species"][0]["planting_density"] = rhiz_density
|
| 292 |
-
config["species"][0]["mortality_rates"].update({
|
| 293 |
-
"year_1": rhiz_mort_1,
|
| 294 |
-
"year_2": rhiz_mort_2,
|
| 295 |
-
"year_3": rhiz_mort_3,
|
| 296 |
-
"year_4": rhiz_mort_4,
|
| 297 |
-
"year_5": rhiz_mort_5
|
| 298 |
-
})
|
| 299 |
-
|
| 300 |
-
# Update Rhizophora Chapman-Richards parameters
|
| 301 |
-
config["species"][0]["chapman_richards"]["dbh"].update({
|
| 302 |
-
"a": rhiz_dbh_a,
|
| 303 |
-
"b": rhiz_dbh_b,
|
| 304 |
-
"c": rhiz_dbh_c
|
| 305 |
-
})
|
| 306 |
-
config["species"][0]["chapman_richards"]["height"].update({
|
| 307 |
-
"a": rhiz_height_a,
|
| 308 |
-
"b": rhiz_height_b,
|
| 309 |
-
"c": rhiz_height_c
|
| 310 |
-
})
|
| 311 |
-
config["species"][0]["initial_values"].update({
|
| 312 |
-
"dbh": rhiz_initial_dbh,
|
| 313 |
-
"height": rhiz_initial_height
|
| 314 |
-
})
|
| 315 |
-
|
| 316 |
-
# Update Avicennia parameters
|
| 317 |
-
config["species"][1]["planting_density"] = avic_density
|
| 318 |
-
config["species"][1]["mortality_rates"].update({
|
| 319 |
-
"year_1": avic_mort_1,
|
| 320 |
-
"year_2": avic_mort_2,
|
| 321 |
-
"year_3": avic_mort_3,
|
| 322 |
-
"year_4": avic_mort_4,
|
| 323 |
-
"year_5": avic_mort_5
|
| 324 |
-
})
|
| 325 |
-
|
| 326 |
-
# Update Avicennia Chapman-Richards parameters
|
| 327 |
-
config["species"][1]["chapman_richards"]["dbh"].update({
|
| 328 |
-
"a": avic_dbh_a,
|
| 329 |
-
"b": avic_dbh_b,
|
| 330 |
-
"c": avic_dbh_c
|
| 331 |
-
})
|
| 332 |
-
config["species"][1]["chapman_richards"]["height"].update({
|
| 333 |
-
"a": avic_height_a,
|
| 334 |
-
"b": avic_height_b,
|
| 335 |
-
"c": avic_height_c
|
| 336 |
-
})
|
| 337 |
-
config["species"][1]["initial_values"].update({
|
| 338 |
-
"dbh": avic_initial_dbh,
|
| 339 |
-
"height": avic_initial_height
|
| 340 |
-
})
|
| 341 |
-
|
| 342 |
-
# Update planting schedule
|
| 343 |
-
config["project"]["planting_schedule"].update({
|
| 344 |
-
"year_1": year_1_area,
|
| 345 |
-
"year_2": year_2_area,
|
| 346 |
-
"year_3": year_3_area,
|
| 347 |
-
"year_4": year_4_area,
|
| 348 |
-
"year_5": year_5_area
|
| 349 |
-
})
|
| 350 |
-
|
| 351 |
-
# Update carbon parameters
|
| 352 |
-
config["carbon"]["buffer_percentage"] = buffer_percentage
|
| 353 |
-
|
| 354 |
-
# Run model and create plots/tables
|
| 355 |
-
results, species_results, scenario_results = run_model(config)
|
| 356 |
-
return create_plots_and_tables(results, species_results, scenario_results, config, year_1_area, year_2_area, year_3_area, year_4_area, year_5_area)
|
| 357 |
-
|
| 358 |
|
| 359 |
-
|
| 360 |
-
"
|
| 361 |
-
with gr.
|
| 362 |
-
|
| 363 |
-
gr.
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
with gr.Row():
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
with gr.Row():
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
with gr.Tab("
|
|
|
|
|
|
|
|
|
|
| 419 |
with gr.Row():
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
with gr.Row():
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
#
|
| 437 |
-
|
| 438 |
-
rhiz_height_a, rhiz_height_b, rhiz_height_c,
|
| 439 |
-
rhiz_initial_dbh, rhiz_initial_height,
|
| 440 |
-
# Chapman-Richards parameters for Avicennia
|
| 441 |
-
avic_dbh_a, avic_dbh_b, avic_dbh_c,
|
| 442 |
-
avic_height_a, avic_height_b, avic_height_c,
|
| 443 |
-
avic_initial_dbh, avic_initial_height
|
| 444 |
-
]
|
| 445 |
-
|
| 446 |
-
outputs = [carbon_plot, annual_plot, biomass_plot, summary_text, annual_table, species_table]
|
| 447 |
-
|
| 448 |
-
# Connect button click event
|
| 449 |
-
update_btn.click(fn=update_model, inputs=inputs, outputs=outputs)
|
| 450 |
-
|
| 451 |
-
# Initialize the UI with default values
|
| 452 |
-
app.load(fn=update_model, inputs=inputs, outputs=outputs)
|
| 453 |
-
|
| 454 |
-
return app
|
| 455 |
-
|
| 456 |
|
| 457 |
if __name__ == "__main__":
|
| 458 |
-
|
| 459 |
-
app.launch(share=True)
|
|
|
|
| 1 |
"""
|
| 2 |
+
Mangrove ER Model Dashboard
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
- Each tab corresponds to a different model/config.
|
| 5 |
+
- To add a new model, add its config to configs/ and add an entry to MODEL_CONFIGS below.
|
| 6 |
+
- Each tab shows the results for the base config (no parameter editing).
|
| 7 |
+
"""
|
| 8 |
import gradio as gr
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from src.er_model import ERModel
|
| 11 |
+
import yaml
|
| 12 |
+
import tempfile
|
| 13 |
import matplotlib.pyplot as plt
|
|
|
|
| 14 |
import pandas as pd
|
| 15 |
+
import numpy as np
|
| 16 |
+
import plotly.graph_objs as go
|
| 17 |
+
from plotly.subplots import make_subplots
|
| 18 |
+
import warnings
|
| 19 |
+
import traceback
|
| 20 |
|
| 21 |
+
MODEL_CONFIGS = {
|
| 22 |
+
"Original Model": "configs/params.yaml",
|
| 23 |
+
"Simple Linear": "configs/linear.yaml",
|
| 24 |
+
"Linear Plateau": "configs/linear_plateau.yaml",
|
| 25 |
+
"Declining Increment": "configs/declining_increment.yaml"
|
| 26 |
+
}
|
| 27 |
|
| 28 |
+
# Helper to update planting schedule in a config file
|
| 29 |
+
def update_planting_schedule(config_path, year_areas):
|
| 30 |
with open(config_path) as f:
|
| 31 |
+
config = yaml.safe_load(f)
|
| 32 |
+
for i, area in enumerate(year_areas, 1):
|
| 33 |
+
config["project"]["planting_schedule"][f"year_{i}"] = area
|
| 34 |
+
return config
|
| 35 |
|
| 36 |
+
def create_survival_table(model):
|
| 37 |
+
"""
|
| 38 |
+
Returns a DataFrame with years as rows and columns for each species and total surviving trees.
|
| 39 |
+
"""
|
| 40 |
+
years = range(1, model.project.duration_years + 1)
|
| 41 |
+
data = {"Year": []}
|
| 42 |
+
species_names = [s.name for s in model.species]
|
| 43 |
+
for name in species_names:
|
| 44 |
+
data[name] = []
|
| 45 |
+
data["Total Surviving Trees"] = []
|
| 46 |
+
for year in years:
|
| 47 |
+
data["Year"].append(year)
|
| 48 |
+
totals = model.calculate_total_surviving_trees(year)
|
| 49 |
+
total = 0
|
| 50 |
+
for name in species_names:
|
| 51 |
+
val = totals.get(name, 0)
|
| 52 |
+
data[name].append(val)
|
| 53 |
+
total += val
|
| 54 |
+
data["Total Surviving Trees"].append(total)
|
| 55 |
+
df = pd.DataFrame(data)
|
| 56 |
+
# Format numbers: no decimals, thousands separator
|
| 57 |
+
for name in species_names + ["Total Surviving Trees"]:
|
| 58 |
+
df[name] = df[name].apply(lambda x: f"{x:,.0f}")
|
| 59 |
+
return df
|
| 60 |
|
| 61 |
+
def run_model_from_config(config):
|
| 62 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
| 63 |
+
yaml.dump(config, tmp)
|
| 64 |
+
tmp_path = tmp.name
|
| 65 |
+
model = ERModel(Path(tmp_path))
|
| 66 |
+
results, species_results = model.run()
|
| 67 |
+
plots = create_all_plots(results, species_results, config)
|
| 68 |
+
summary = create_summary(results, species_results, config, model)
|
| 69 |
+
survival_table = create_survival_table(model)
|
| 70 |
+
# Format tables
|
| 71 |
+
results_fmt = results.copy()
|
| 72 |
+
species_results_fmt = species_results.copy()
|
| 73 |
+
for col in results_fmt.columns:
|
| 74 |
+
if results_fmt[col].dtype in [float, int]:
|
| 75 |
+
results_fmt[col] = results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
|
| 76 |
+
for col in species_results_fmt.columns:
|
| 77 |
+
if species_results_fmt[col].dtype in [float, int]:
|
| 78 |
+
species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
|
| 79 |
+
return (*plots, summary, results_fmt.head(30), species_results_fmt.head(30), survival_table.head(30))
|
| 80 |
|
| 81 |
+
def run_model(config_path):
|
| 82 |
+
with open(config_path) as f:
|
| 83 |
+
config = yaml.safe_load(f)
|
| 84 |
+
model = ERModel(Path(config_path))
|
| 85 |
+
results, species_results = model.run()
|
| 86 |
+
plots = create_all_plots(results, species_results, config)
|
| 87 |
+
summary = create_summary(results, species_results, config, model)
|
| 88 |
+
survival_table = create_survival_table(model)
|
| 89 |
+
# Format tables
|
| 90 |
+
results_fmt = results.copy()
|
| 91 |
+
species_results_fmt = species_results.copy()
|
| 92 |
+
for col in results_fmt.columns:
|
| 93 |
+
if results_fmt[col].dtype in [float, int]:
|
| 94 |
+
results_fmt[col] = results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
|
| 95 |
+
for col in species_results_fmt.columns:
|
| 96 |
+
if species_results_fmt[col].dtype in [float, int]:
|
| 97 |
+
species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
|
| 98 |
+
return (*plots, summary, results_fmt.head(30), species_results_fmt.head(30), survival_table.head(30))
|
| 99 |
|
| 100 |
+
def check_complex(arr, label):
|
| 101 |
+
if np.iscomplexobj(arr):
|
| 102 |
+
complex_indices = np.where(np.iscomplex(arr))[0]
|
| 103 |
+
warnings.warn(f"[ERROR] Complex values in {label}: indices={complex_indices}, values={arr[complex_indices]}")
|
| 104 |
+
traceback.print_stack()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
def create_growth_increment_plots(config, model_type=None):
|
| 107 |
+
"""
|
| 108 |
+
Create a 2x2 grid of growth and increment plots for DBH and Height using Plotly.
|
| 109 |
+
model_type: 'chapman_richards', 'linear', 'linear_plateau', or 'declining_increment'
|
| 110 |
+
"""
|
| 111 |
+
if model_type is None:
|
| 112 |
+
model_type = config.get('growth_model', 'chapman_richards')
|
| 113 |
+
years = np.arange(1, config["project"]["duration_years"] + 1)
|
| 114 |
+
fig = make_subplots(rows=2, cols=2, subplot_titles=("DBH Growth", "HEIGHT Growth", "DBH Annual Increment", "HEIGHT Annual Increment"))
|
| 115 |
+
from src.er_model import ERModel
|
| 116 |
+
for sp in config["species"]:
|
| 117 |
+
name = sp["name"]
|
| 118 |
+
initial_dbh = sp["initial_values"]["dbh"]
|
| 119 |
+
initial_height = sp["initial_values"]["height"]
|
| 120 |
+
if model_type == "linear":
|
| 121 |
+
dbh_params = sp["linear"]["dbh"]
|
| 122 |
+
height_params = sp["linear"]["height"]
|
| 123 |
+
dbh = [ERModel.linear_growth(t, dbh_params, initial_dbh) for t in years]
|
| 124 |
+
height = [ERModel.linear_growth(t, height_params, initial_height) for t in years]
|
| 125 |
+
elif model_type == "linear_plateau":
|
| 126 |
+
dbh_params = sp["linear_plateau"]["dbh"]
|
| 127 |
+
height_params = sp["linear_plateau"]["height"]
|
| 128 |
+
dbh = [ERModel.linear_plateau_growth(t, dbh_params, initial_dbh) for t in years]
|
| 129 |
+
height = [ERModel.linear_plateau_growth(t, height_params, initial_height) for t in years]
|
| 130 |
+
elif model_type == "declining_increment":
|
| 131 |
+
dbh_params = sp["declining_increment"]["dbh"]
|
| 132 |
+
height_params = sp["declining_increment"]["height"]
|
| 133 |
+
dbh = [ERModel.declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
|
| 134 |
+
height = [ERModel.declining_increment_growth(t, height_params, initial_height) for t in years]
|
| 135 |
+
else: # Default to Chapman-Richards
|
| 136 |
+
dbh_params = sp["chapman_richards"]["dbh"]
|
| 137 |
+
height_params = sp["chapman_richards"]["height"]
|
| 138 |
+
def chapman_richards(t, params, initial):
|
| 139 |
+
a, b, c = params["a"], params["b"], params["c"]
|
| 140 |
+
return initial + (a - initial) * (1 - np.exp(-b * t)) ** c
|
| 141 |
+
dbh = [chapman_richards(t, dbh_params, initial_dbh) for t in years]
|
| 142 |
+
height = [chapman_richards(t, height_params, initial_height) for t in years]
|
| 143 |
+
dbh = np.array(dbh)
|
| 144 |
+
height = np.array(height)
|
| 145 |
+
check_complex(dbh, f"{name} DBH (growth)")
|
| 146 |
+
check_complex(height, f"{name} Height (growth)")
|
| 147 |
+
dbh_inc = np.diff(np.insert(dbh, 0, dbh[0]))
|
| 148 |
+
height_inc = np.diff(np.insert(height, 0, height[0]))
|
| 149 |
+
check_complex(dbh_inc, f"{name} DBH Δ (increment)")
|
| 150 |
+
check_complex(height_inc, f"{name} Height Δ (increment)")
|
| 151 |
+
fig.add_trace(go.Scatter(x=years, y=dbh, mode='lines', name=f"{name} DBH", legendgroup=name, line=dict(width=2)), row=1, col=1)
|
| 152 |
+
fig.add_trace(go.Scatter(x=years, y=height, mode='lines', name=f"{name} Height", legendgroup=name, line=dict(width=2)), row=1, col=2)
|
| 153 |
+
fig.add_trace(go.Scatter(x=years, y=dbh_inc, mode='lines', name=f"{name} DBH Δ", legendgroup=name, showlegend=False, line=dict(width=2)), row=2, col=1)
|
| 154 |
+
fig.add_trace(go.Scatter(x=years, y=height_inc, mode='lines', name=f"{name} Height Δ", legendgroup=name, showlegend=False, line=dict(width=2)), row=2, col=2)
|
| 155 |
+
fig.update_layout(height=700, width=900, title_text="Growth and Increment Curves", hovermode="x unified")
|
| 156 |
+
fig.update_xaxes(title_text="Age (years)", row=1, col=1)
|
| 157 |
+
fig.update_xaxes(title_text="Age (years)", row=1, col=2)
|
| 158 |
+
fig.update_xaxes(title_text="Age (years)", row=2, col=1)
|
| 159 |
+
fig.update_xaxes(title_text="Age (years)", row=2, col=2)
|
| 160 |
+
fig.update_yaxes(title_text="Size (cm)", row=1, col=1)
|
| 161 |
+
fig.update_yaxes(title_text="Size (m)", row=1, col=2)
|
| 162 |
+
fig.update_yaxes(title_text="Annual increment (cm/year)", row=2, col=1)
|
| 163 |
+
fig.update_yaxes(title_text="Annual increment (m/year)", row=2, col=2)
|
| 164 |
+
return fig
|
| 165 |
|
| 166 |
+
def create_all_plots(results, species_results, config):
|
| 167 |
+
# Diagnostic: Check for complex numbers in results DataFrame columns used for plotting
|
| 168 |
+
for col in ["gross_carbon", "net_carbon"]:
|
| 169 |
+
arr = np.array(results[col])
|
| 170 |
+
check_complex(arr, f"results['{col}']")
|
| 171 |
+
# 1. Carbon curve (gross and net)
|
| 172 |
+
fig1 = go.Figure()
|
| 173 |
+
fig1.add_trace(go.Scatter(x=results["year"], y=results["gross_carbon"], mode="lines+markers", name="Gross Carbon", line=dict(width=2)))
|
| 174 |
+
fig1.add_trace(go.Scatter(x=results["year"], y=results["net_carbon"], mode="lines+markers", name="Net Carbon", line=dict(width=2)))
|
| 175 |
+
fig1.update_layout(title="Carbon Sequestration Over Time", xaxis_title="Year", yaxis_title="Carbon (tCO2)", hovermode="x unified", template="plotly_white")
|
| 176 |
+
|
| 177 |
+
# 2. Annual carbon (ERs)
|
| 178 |
+
annual_ers = results["net_carbon"].diff().fillna(results["net_carbon"].iloc[0])
|
| 179 |
+
arr = np.array(annual_ers)
|
| 180 |
+
check_complex(arr, "annual_ers")
|
| 181 |
+
fig2 = go.Figure()
|
| 182 |
+
fig2.add_trace(go.Bar(x=results["year"], y=annual_ers, name="Annual ERs", marker_color="#2ecc71", opacity=0.7))
|
| 183 |
+
fig2.update_layout(title="Annual Emission Reductions", xaxis_title="Year", yaxis_title="Annual ERs (tCO2)", hovermode="x unified", template="plotly_white")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
+
# 3. Biomass per tree for all species
|
| 186 |
+
years = results["year"]
|
| 187 |
+
fig3 = go.Figure()
|
| 188 |
+
from src.er_model import ERModel
|
| 189 |
+
growth_model = config.get('growth_model', 'chapman_richards')
|
| 190 |
+
for sp in config["species"]:
|
| 191 |
+
name = sp["name"]
|
| 192 |
+
initial_dbh = sp["initial_values"]["dbh"]
|
| 193 |
+
initial_height = sp["initial_values"]["height"]
|
| 194 |
+
if growth_model == "linear":
|
| 195 |
+
dbh_params = sp["linear"]["dbh"]
|
| 196 |
+
height_params = sp["linear"]["height"]
|
| 197 |
+
dbh = [ERModel.linear_growth(t, dbh_params, initial_dbh) for t in years]
|
| 198 |
+
height = [ERModel.linear_growth(t, height_params, initial_height) for t in years]
|
| 199 |
+
elif growth_model == "linear_plateau":
|
| 200 |
+
dbh_params = sp["linear_plateau"]["dbh"]
|
| 201 |
+
height_params = sp["linear_plateau"]["height"]
|
| 202 |
+
dbh = [ERModel.linear_plateau_growth(t, dbh_params, initial_dbh) for t in years]
|
| 203 |
+
height = [ERModel.linear_plateau_growth(t, height_params, initial_height) for t in years]
|
| 204 |
+
elif growth_model == "declining_increment":
|
| 205 |
+
dbh_params = sp["declining_increment"]["dbh"]
|
| 206 |
+
height_params = sp["declining_increment"]["height"]
|
| 207 |
+
dbh = [ERModel.declining_increment_growth(t, dbh_params, initial_dbh) for t in years]
|
| 208 |
+
height = [ERModel.declining_increment_growth(t, height_params, initial_height) for t in years]
|
| 209 |
+
else: # Default to Chapman-Richards
|
| 210 |
+
dbh_params = sp["chapman_richards"]["dbh"]
|
| 211 |
+
height_params = sp["chapman_richards"]["height"]
|
| 212 |
+
def chapman_richards(t, params, initial):
|
| 213 |
+
a, b, c = params["a"], params["b"], params["c"]
|
| 214 |
+
return initial + (a - initial) * (1 - np.exp(-b * t)) ** c
|
| 215 |
+
dbh = [chapman_richards(t, dbh_params, initial_dbh) for t in years]
|
| 216 |
+
height = [chapman_richards(t, height_params, initial_height) for t in years]
|
| 217 |
+
if "Zanvo" in sp["allometry"]["equation"]:
|
| 218 |
+
if "1.938" in sp["allometry"]["equation"]:
|
| 219 |
+
biomass = 1.938 * (np.array(dbh) ** 2 * np.array(height)) ** 0.67628 / 1000
|
| 220 |
+
else:
|
| 221 |
+
biomass = 1.486 * (np.array(dbh) ** 2 * np.array(height)) ** 0.55864 / 1000
|
| 222 |
+
else:
|
| 223 |
+
biomass = dbh
|
| 224 |
+
check_complex(biomass, f"{name} biomass (allometry)")
|
| 225 |
+
fig3.add_trace(go.Scatter(x=years, y=biomass, mode="lines+markers", name=name, line=dict(width=2)))
|
| 226 |
+
fig3.update_layout(title="Total Biomass per Tree", xaxis_title="Year since planting", yaxis_title="Biomass (tonnes per tree)", hovermode="x unified", template="plotly_white")
|
| 227 |
+
return (fig1, fig2, fig3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
+
def create_summary(results, species_results, config, model):
|
| 230 |
+
total_area = sum(config["project"]["planting_schedule"].values())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
final_gross = results["gross_carbon"].iloc[-1]
|
| 232 |
final_net = results["net_carbon"].iloc[-1]
|
| 233 |
+
years = len(results)
|
| 234 |
+
gross_carbon_per_ha = final_gross / total_area if total_area > 0 else 0
|
| 235 |
+
net_carbon_per_ha = final_net / total_area if total_area > 0 else 0
|
| 236 |
+
annual_net_per_ha = net_carbon_per_ha / years if years > 0 else 0
|
| 237 |
+
# Milestone years
|
| 238 |
+
def get_val(col, idx):
|
| 239 |
+
return results[col].iloc[idx] if len(results) > idx else 0
|
| 240 |
+
year_5_gross = get_val("gross_carbon", 4)
|
| 241 |
+
year_5_net = get_val("net_carbon", 4)
|
| 242 |
+
year_10_gross = get_val("gross_carbon", 9)
|
| 243 |
+
year_10_net = get_val("net_carbon", 9)
|
| 244 |
+
year_20_gross = get_val("gross_carbon", 19)
|
| 245 |
+
year_20_net = get_val("net_carbon", 19)
|
| 246 |
+
avg_annual_gross = final_gross / years if years > 0 else 0
|
| 247 |
+
avg_annual_net = final_net / years if years > 0 else 0
|
| 248 |
+
# Surviving trees at milestones
|
| 249 |
+
milestones = [5, 10, 20, 30]
|
| 250 |
+
survival_lines = []
|
| 251 |
+
for m in milestones:
|
| 252 |
+
if m <= years:
|
| 253 |
+
surv = model.calculate_total_surviving_trees(m)
|
| 254 |
+
surv_str = ", ".join([f"{k}: {v:,.0f}" for k, v in surv.items()])
|
| 255 |
+
survival_lines.append(f"Year {m} surviving trees: {surv_str}")
|
| 256 |
+
# Per hectare (final year)
|
| 257 |
+
surv_30 = model.calculate_total_surviving_trees(years)
|
| 258 |
+
per_ha_lines = []
|
| 259 |
+
for k, v in surv_30.items():
|
| 260 |
+
per_ha = v / total_area if total_area > 0 else 0
|
| 261 |
+
per_ha_lines.append(f"{k}: {per_ha:,.0f} trees/ha")
|
| 262 |
+
summary = (
|
| 263 |
+
f"Project Overview:\n----------------\nDuration: {years} years\nTotal Area Planted: {total_area:,.0f} ha\nBuffer Pool: {config['carbon']['buffer_percentage']}%\n\n"
|
| 264 |
+
f"Carbon Sequestration:\n-------------------\nTotal Gross Carbon (Year {years}): {final_gross:,.0f} tCO2\nTotal Net Carbon (Year {years}): {final_net:,.0f} tCO2\nAverage Annual Gross: {avg_annual_gross:,.0f} tCO2/yr\nAverage Annual Net: {avg_annual_net:,.0f} tCO2/yr\n\n"
|
| 265 |
+
f"Milestone Years:\n--------------\nYear 5 Gross: {year_5_gross:,.0f} tCO2\nYear 5 Net: {year_5_net:,.0f} tCO2\nYear 10 Gross: {year_10_gross:,.0f} tCO2\nYear 10 Net: {year_10_net:,.0f} tCO2\nYear 20 Gross: {year_20_gross:,.0f} tCO2\nYear 20 Net: {year_20_net:,.0f} tCO2\n\n"
|
| 266 |
+
"Surviving Trees (Milestones):\n----------------------------\n"
|
| 267 |
+
+ "\n".join(survival_lines)
|
| 268 |
+
+ "\n\nPer Hectare Surviving Trees (Final Year):\n----------------------------------------\n"
|
| 269 |
+
+ "\n".join(per_ha_lines)
|
| 270 |
+
+ f"\n\nPer Hectare Metrics (Year {years}):\n-----------------------------\nGross Carbon per ha: {gross_carbon_per_ha:,.0f} tCO2/ha\nNet Carbon per ha: {net_carbon_per_ha:,.0f} tCO2/ha\nAverage Annual Net per ha: {annual_net_per_ha:,.2f} tCO2/ha/yr\n"
|
| 271 |
+
)
|
| 272 |
+
return summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
+
with gr.Blocks() as demo:
|
| 275 |
+
gr.Markdown("# Mangrove ER Model Dashboard\nSelect a model tab to view results for that model's base configuration.")
|
| 276 |
+
with gr.Tabs():
|
| 277 |
+
# Original Model tab with planting schedule editing
|
| 278 |
+
with gr.Tab("Original Model"):
|
| 279 |
+
gr.Markdown("## Planting Schedule (ha per year)")
|
| 280 |
+
year_1 = gr.Number(value=2500, label="Year 1 Area (ha)")
|
| 281 |
+
year_2 = gr.Number(value=2500, label="Year 2 Area (ha)")
|
| 282 |
+
year_3 = gr.Number(value=0, label="Year 3 Area (ha)")
|
| 283 |
+
year_4 = gr.Number(value=0, label="Year 4 Area (ha)")
|
| 284 |
+
year_5 = gr.Number(value=0, label="Year 5 Area (ha)")
|
| 285 |
+
update_btn = gr.Button("Update Results", variant="primary")
|
| 286 |
with gr.Row():
|
| 287 |
+
carbon_plot = gr.Plot()
|
| 288 |
+
annual_plot = gr.Plot()
|
| 289 |
+
biomass_plot = gr.Plot()
|
| 290 |
+
# Now re-enable carbon_plot as well
|
| 291 |
+
# growth_plot = gr.Plot(label="Growth & Increment Curves")
|
| 292 |
+
summary_box = gr.Textbox(label="Summary", lines=12)
|
| 293 |
+
results_box = gr.Dataframe()
|
| 294 |
+
species_box = gr.Dataframe()
|
| 295 |
+
survival_box = gr.Dataframe(label="Surviving Trees Table")
|
| 296 |
+
def update_original_model(y1, y2, y3, y4, y5):
|
| 297 |
+
config = update_planting_schedule(MODEL_CONFIGS["Original Model"], [y1, y2, y3, y4, y5])
|
| 298 |
+
import tempfile
|
| 299 |
+
import yaml
|
| 300 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
| 301 |
+
yaml.dump(config, tmp)
|
| 302 |
+
tmp_path = tmp.name
|
| 303 |
+
model = ERModel(Path(tmp_path))
|
| 304 |
+
results, species_results = model.run()
|
| 305 |
+
plots = create_all_plots(results, species_results, config)
|
| 306 |
+
summary = create_summary(results, species_results, config, model)
|
| 307 |
+
survival_table = create_survival_table(model)
|
| 308 |
+
results_fmt = results.copy()
|
| 309 |
+
species_results_fmt = species_results.copy()
|
| 310 |
+
for col in results_fmt.columns:
|
| 311 |
+
if results_fmt[col].dtype in [float, int]:
|
| 312 |
+
results_fmt[col] = results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
|
| 313 |
+
for col in species_results_fmt.columns:
|
| 314 |
+
if species_results_fmt[col].dtype in [float, int]:
|
| 315 |
+
species_results_fmt[col] = species_results_fmt[col].apply(lambda x: f"{x:,.2f}" if isinstance(x, float) else f"{x:,}")
|
| 316 |
+
# Return the carbon plot (plots[0]), biomass plot (plots[2]), and annual plot (plots[1])
|
| 317 |
+
return plots[0], plots[2], plots[1], summary, results_fmt.head(30), species_results_fmt.head(30), survival_table.head(30)
|
| 318 |
+
update_btn.click(
|
| 319 |
+
update_original_model,
|
| 320 |
+
inputs=[year_1, year_2, year_3, year_4, year_5],
|
| 321 |
+
outputs=[carbon_plot, biomass_plot, annual_plot, summary_box, results_box, species_box, survival_box]
|
| 322 |
+
)
|
| 323 |
+
# Show initial results
|
| 324 |
+
c, a, b, summary, r, s, surv = run_model(MODEL_CONFIGS["Original Model"])
|
| 325 |
+
# Assign the carbon plot (c), biomass plot (b), and annual plot (a)
|
| 326 |
+
carbon_plot.value = c
|
| 327 |
+
biomass_plot.value = b
|
| 328 |
+
annual_plot.value = a
|
| 329 |
+
summary_box.value = summary
|
| 330 |
+
results_box.value = r
|
| 331 |
+
species_box.value = s
|
| 332 |
+
survival_box.value = surv
|
| 333 |
+
# Simple Linear tab
|
| 334 |
+
with gr.Tab("Simple Linear"):
|
| 335 |
+
gr.Markdown("## Simple Linear Model Results (base config)")
|
| 336 |
+
carbon_plot, annual_plot, biomass_plot, summary, results, species, survival = run_model(MODEL_CONFIGS["Simple Linear"])
|
| 337 |
+
growth_fig = create_growth_increment_plots(yaml.safe_load(open(MODEL_CONFIGS["Simple Linear"])), model_type="linear")
|
| 338 |
with gr.Row():
|
| 339 |
+
carbon_plot_comp = gr.Plot(value=carbon_plot)
|
| 340 |
+
biomass_plot_comp = gr.Plot(value=biomass_plot)
|
| 341 |
+
annual_plot_comp = gr.Plot(value=annual_plot)
|
| 342 |
+
# gr.Plot(value=growth_fig, label="Growth & Increment Curves")
|
| 343 |
+
gr.Textbox(value=summary, label="Summary", lines=12)
|
| 344 |
+
gr.Markdown("### Results Table")
|
| 345 |
+
gr.Dataframe(value=results)
|
| 346 |
+
gr.Markdown("### Species Table")
|
| 347 |
+
gr.Dataframe(value=species)
|
| 348 |
+
gr.Markdown("### Surviving Trees Table")
|
| 349 |
+
gr.Dataframe(value=survival)
|
| 350 |
+
# Linear Plateau tab
|
| 351 |
+
with gr.Tab("Linear Plateau"):
|
| 352 |
+
gr.Markdown("## Linear Plateau Model Results (base config)")
|
| 353 |
+
carbon_plot, annual_plot, biomass_plot, summary, results, species, survival = run_model(MODEL_CONFIGS["Linear Plateau"])
|
| 354 |
+
growth_fig = create_growth_increment_plots(yaml.safe_load(open(MODEL_CONFIGS["Linear Plateau"])), model_type="linear_plateau")
|
| 355 |
with gr.Row():
|
| 356 |
+
carbon_plot_comp = gr.Plot(value=carbon_plot)
|
| 357 |
+
biomass_plot_comp = gr.Plot(value=biomass_plot)
|
| 358 |
+
annual_plot_comp = gr.Plot(value=annual_plot)
|
| 359 |
+
# gr.Plot(value=growth_fig, label="Growth & Increment Curves")
|
| 360 |
+
gr.Textbox(value=summary, label="Summary", lines=12)
|
| 361 |
+
gr.Markdown("### Results Table")
|
| 362 |
+
gr.Dataframe(value=results)
|
| 363 |
+
gr.Markdown("### Species Table")
|
| 364 |
+
gr.Dataframe(value=species)
|
| 365 |
+
gr.Markdown("### Surviving Trees Table")
|
| 366 |
+
gr.Dataframe(value=survival)
|
| 367 |
+
# Declining Increment tab
|
| 368 |
+
with gr.Tab("Declining Increment"):
|
| 369 |
+
gr.Markdown("## Declining Increment Model Results (base config)")
|
| 370 |
+
carbon_plot, annual_plot, biomass_plot, summary, results, species, survival = run_model(MODEL_CONFIGS["Declining Increment"])
|
| 371 |
+
growth_fig = create_growth_increment_plots(yaml.safe_load(open(MODEL_CONFIGS["Declining Increment"])), model_type="declining_increment")
|
| 372 |
+
# Diagnostics for DBH, height, and biomass arrays before plotting (biomass plot)
|
| 373 |
+
config = yaml.safe_load(open(MODEL_CONFIGS["Declining Increment"]))
|
| 374 |
+
years = results["year"]
|
| 375 |
+
from src.er_model import ERModel
|
| 376 |
+
for sp in config["species"]:
|
| 377 |
+
name = sp["name"]
|
| 378 |
+
initial_dbh = sp["initial_values"]["dbh"]
|
| 379 |
+
initial_height = sp["initial_values"]["height"]
|
| 380 |
+
dbh_params = sp["declining_increment"]["dbh"]
|
| 381 |
+
height_params = sp["declining_increment"]["height"]
|
| 382 |
+
dbh = np.array([ERModel.declining_increment_growth(float(t), dbh_params, initial_dbh) for t in years])
|
| 383 |
+
height = np.array([ERModel.declining_increment_growth(float(t), height_params, initial_height) for t in years])
|
| 384 |
+
if "Zanvo" in sp["allometry"]["equation"]:
|
| 385 |
+
if "1.938" in sp["allometry"]["equation"]:
|
| 386 |
+
biomass = 1.938 * (dbh ** 2 * height) ** 0.67628 / 1000
|
| 387 |
+
else:
|
| 388 |
+
biomass = 1.486 * (dbh ** 2 * height) ** 0.55864 / 1000
|
| 389 |
+
else:
|
| 390 |
+
biomass = dbh
|
| 391 |
+
print(f"[DEBUG] {name} DBH: min={dbh.min()}, max={dbh.max()}, any_negative={np.any(dbh < 0)}, any_complex={np.iscomplexobj(dbh)}")
|
| 392 |
+
print(f"[DEBUG] {name} Height: min={height.min()}, max={height.max()}, any_negative={np.any(height < 0)}, any_complex={np.iscomplexobj(height)}")
|
| 393 |
+
print(f"[DEBUG] {name} Biomass: min={biomass.min()}, max={biomass.max()}, any_negative={np.any(biomass < 0)}, any_complex={np.iscomplexobj(biomass)}")
|
| 394 |
with gr.Row():
|
| 395 |
+
carbon_plot_comp = gr.Plot(value=carbon_plot)
|
| 396 |
+
biomass_plot_comp = gr.Plot(value=biomass_plot)
|
| 397 |
+
annual_plot_comp = gr.Plot(value=annual_plot)
|
| 398 |
+
# gr.Plot(value=growth_fig, label="Growth & Increment Curves")
|
| 399 |
+
gr.Textbox(value=summary, label="Summary", lines=12)
|
| 400 |
+
gr.Markdown("### Results Table")
|
| 401 |
+
gr.Dataframe(value=results)
|
| 402 |
+
gr.Markdown("### Species Table")
|
| 403 |
+
gr.Dataframe(value=species)
|
| 404 |
+
gr.Markdown("### Surviving Trees Table")
|
| 405 |
+
gr.Dataframe(value=survival)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
if __name__ == "__main__":
|
| 408 |
+
demo.launch()
|
|
|
models/original/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Original Model
|
| 2 |
+
|
| 3 |
+
## Model Overview
|
| 4 |
+
This is the default Chapman-Richards-based mangrove growth and carbon model for the project. It uses species-specific growth curves and DBH-dependent mortality to estimate carbon sequestration and tree survival over time.
|
| 5 |
+
|
| 6 |
+
## Growth & Mortality Logic
|
| 7 |
+
- **Growth:**
|
| 8 |
+
- Uses Chapman-Richards equations for both DBH and height for each species.
|
| 9 |
+
- Parameters are derived from field data and literature.
|
| 10 |
+
- **Mortality:**
|
| 11 |
+
- Applies a DBH-dependent mortality rate, where annual mortality is a function of tree size (DBH) and its increment.
|
| 12 |
+
- This dynamic approach reflects higher mortality when trees are small, leveling off as they mature.
|
| 13 |
+
|
| 14 |
+
## Parameters
|
| 15 |
+
- See `configs/params.yaml` for all parameter values, including:
|
| 16 |
+
- Chapman-Richards parameters for DBH and height
|
| 17 |
+
- Initial values for DBH and height
|
| 18 |
+
- Planting density
|
| 19 |
+
- Mortality parameters (m_ref, DBH_ref, p)
|
| 20 |
+
- Allometric equations for biomass
|
| 21 |
+
|
| 22 |
+
## References
|
| 23 |
+
- Chapman-Richards growth model literature
|
| 24 |
+
- Zanvo et al. (2023) for allometric equations
|
| 25 |
+
- Field data from Nigeria mangrove sites (2024)
|
| 26 |
+
|
| 27 |
+
## Usage
|
| 28 |
+
- This model is available as the "Original Model" tab in the dashboard.
|
| 29 |
+
- To modify parameters, edit `configs/params.yaml`.
|
| 30 |
+
- For more details on running the dashboard, see the top-level README.
|
src/allometry.py
CHANGED
|
@@ -17,6 +17,9 @@ def calculate_biomass(dbh: float, height: float, species_name: str, params: Dict
|
|
| 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
|
|
|
|
| 17 |
Returns:
|
| 18 |
Total tree biomass (above + below ground) in kg
|
| 19 |
"""
|
| 20 |
+
base = dbh**2 * height
|
| 21 |
+
if base <= 0:
|
| 22 |
+
print(f"[ERROR] Non-positive base in calculate_biomass: dbh={dbh}, height={height}, base={base}, species={species_name}")
|
| 23 |
# Use Zanvo et al. 2023 equations that include both DBH and height
|
| 24 |
if species_name == "species_A": # Rhizophora
|
| 25 |
# Total = 1.938 × (DBH² H)^0.67628
|
src/current_er_model (1).ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/er_model.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Dict, List, Optional, Tuple
|
|
| 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
|
|
@@ -18,10 +19,18 @@ class Species:
|
|
| 18 |
"""Species-specific parameters for growth and carbon calculations."""
|
| 19 |
name: str
|
| 20 |
planting_density: float
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
@dataclass
|
|
@@ -76,32 +85,73 @@ class ERModel:
|
|
| 76 |
self.species_results: Optional[pd.DataFrame] = None
|
| 77 |
self.scenario_results: Optional[pd.DataFrame] = None
|
| 78 |
|
| 79 |
-
|
|
|
|
|
|
|
| 80 |
"""
|
| 81 |
Calculate surviving trees for a cohort planted in planting_year, in current_year.
|
| 82 |
-
|
|
|
|
| 83 |
"""
|
|
|
|
|
|
|
| 84 |
age = current_year - planting_year + 1
|
| 85 |
if age < 1:
|
| 86 |
return 0
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
def calculate_total_surviving_trees(self, year: int) -> Dict[str, float]:
|
| 94 |
"""
|
| 95 |
Calculate total surviving trees for each species in a given year, summing across all cohorts.
|
| 96 |
Returns a dict: {species_name: total_surviving_trees}
|
| 97 |
"""
|
|
|
|
| 98 |
totals = {}
|
| 99 |
for species in self.species:
|
| 100 |
total = 0
|
| 101 |
for planting_year, area in self.project.planting_schedule.items():
|
| 102 |
py = int(planting_year.split("_")[1])
|
| 103 |
initial_trees = species.planting_density * area
|
| 104 |
-
|
|
|
|
|
|
|
| 105 |
totals[species.name] = total
|
| 106 |
return totals
|
| 107 |
|
|
@@ -116,21 +166,43 @@ class ERModel:
|
|
| 116 |
total += area
|
| 117 |
return total
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
Calculate growth using Chapman-Richards growth equation.
|
| 122 |
-
|
| 123 |
-
Args:
|
| 124 |
-
age: Tree age in years
|
| 125 |
-
params: Dictionary with a, b, c parameters
|
| 126 |
-
initial_value: Initial value (DBH or height)
|
| 127 |
-
|
| 128 |
-
Returns:
|
| 129 |
-
Current size (DBH in cm or height in m)
|
| 130 |
-
"""
|
| 131 |
a, b, c = params["a"], params["b"], params["c"]
|
| 132 |
return initial_value + (a - initial_value) * (1 - np.exp(-b * age)) ** c
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
def calculate_carbon_for_species(self, species: Species, age: int, area: float) -> float:
|
| 135 |
"""
|
| 136 |
Calculate carbon sequestration for a single species and age.
|
|
@@ -141,23 +213,15 @@ class ERModel:
|
|
| 141 |
Returns:
|
| 142 |
Carbon sequestration in tCO2
|
| 143 |
"""
|
| 144 |
-
# Calculate surviving trees using
|
| 145 |
initial_trees = species.planting_density * area
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
dbh =
|
| 152 |
-
|
| 153 |
-
species.chapman_richards["dbh"],
|
| 154 |
-
species.initial_values["dbh"]
|
| 155 |
-
)
|
| 156 |
-
height = self.chapman_richards_growth(
|
| 157 |
-
age,
|
| 158 |
-
species.chapman_richards["height"],
|
| 159 |
-
species.initial_values["height"]
|
| 160 |
-
)
|
| 161 |
# Calculate biomass using both DBH and height
|
| 162 |
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
| 163 |
# Convert to carbon
|
|
@@ -168,66 +232,11 @@ class ERModel:
|
|
| 168 |
)
|
| 169 |
return carbon
|
| 170 |
|
| 171 |
-
def
|
| 172 |
-
growth_rate_factor: float) -> pd.Series:
|
| 173 |
-
"""
|
| 174 |
-
Run a scenario with modified parameters.
|
| 175 |
-
|
| 176 |
-
Args:
|
| 177 |
-
area: Area to plant in hectares
|
| 178 |
-
dbh_range: [min_dbh, max_dbh] in cm
|
| 179 |
-
height_range: [min_height, max_height] in m
|
| 180 |
-
growth_rate_factor: Factor to multiply growth rate by
|
| 181 |
-
|
| 182 |
-
Returns:
|
| 183 |
-
Series with carbon sequestration at milestone years
|
| 184 |
-
"""
|
| 185 |
-
# Create modified species for scenario
|
| 186 |
-
scenario_species = []
|
| 187 |
-
for sp in self.species:
|
| 188 |
-
mod_sp = Species(
|
| 189 |
-
name=sp.name,
|
| 190 |
-
planting_density=sp.planting_density,
|
| 191 |
-
mortality_rates=sp.mortality_rates.copy(),
|
| 192 |
-
chapman_richards={
|
| 193 |
-
"dbh": {
|
| 194 |
-
"a": np.mean(dbh_range),
|
| 195 |
-
"b": sp.chapman_richards["dbh"]["b"] * growth_rate_factor,
|
| 196 |
-
"c": sp.chapman_richards["dbh"]["c"]
|
| 197 |
-
},
|
| 198 |
-
"height": {
|
| 199 |
-
"a": np.mean(height_range),
|
| 200 |
-
"b": sp.chapman_richards["height"]["b"] * growth_rate_factor,
|
| 201 |
-
"c": sp.chapman_richards["height"]["c"]
|
| 202 |
-
}
|
| 203 |
-
},
|
| 204 |
-
allometry=sp.allometry.copy(),
|
| 205 |
-
initial_values=sp.initial_values.copy()
|
| 206 |
-
)
|
| 207 |
-
scenario_species.append(mod_sp)
|
| 208 |
-
|
| 209 |
-
# Calculate carbon at milestone years
|
| 210 |
-
milestones = [5, 10, 15, 30]
|
| 211 |
-
carbon_values = []
|
| 212 |
-
|
| 213 |
-
for year in milestones:
|
| 214 |
-
total_carbon = sum(
|
| 215 |
-
self.calculate_carbon_for_species(sp, year, area)
|
| 216 |
-
for sp in scenario_species
|
| 217 |
-
)
|
| 218 |
-
carbon_values.append(total_carbon)
|
| 219 |
-
|
| 220 |
-
return pd.Series(
|
| 221 |
-
carbon_values,
|
| 222 |
-
index=[f"Year {y} tCO2" for y in milestones]
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
def run(self) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
| 226 |
"""
|
| 227 |
Execute the full ER calculation pipeline.
|
| 228 |
-
|
| 229 |
Returns:
|
| 230 |
-
Tuple of (yearly results DataFrame, species results DataFrame
|
| 231 |
"""
|
| 232 |
years = range(1, self.project.duration_years + 1)
|
| 233 |
results = []
|
|
@@ -245,22 +254,14 @@ class ERModel:
|
|
| 245 |
for planting_year, area in self.project.planting_schedule.items():
|
| 246 |
py = int(planting_year.split("_")[1])
|
| 247 |
initial_trees = species.planting_density * area
|
| 248 |
-
|
| 249 |
-
if
|
| 250 |
-
|
| 251 |
-
# Calculate
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
dbh =
|
| 255 |
-
|
| 256 |
-
species.chapman_richards["dbh"],
|
| 257 |
-
species.initial_values["dbh"]
|
| 258 |
-
)
|
| 259 |
-
height = self.chapman_richards_growth(
|
| 260 |
-
age,
|
| 261 |
-
species.chapman_richards["height"],
|
| 262 |
-
species.initial_values["height"]
|
| 263 |
-
)
|
| 264 |
# Calculate biomass using both DBH and height
|
| 265 |
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
| 266 |
# Convert to carbon
|
|
@@ -298,58 +299,7 @@ class ERModel:
|
|
| 298 |
self.results = pd.DataFrame(results)
|
| 299 |
self.species_results = pd.DataFrame(species_results)
|
| 300 |
|
| 301 |
-
|
| 302 |
-
scenarios = []
|
| 303 |
-
dbh_min, dbh_max = self.scenarios["dbh_range"]
|
| 304 |
-
height_min, height_max = self.scenarios["height_range"]
|
| 305 |
-
area = self.scenarios["area"]
|
| 306 |
-
growth_factor = self.scenarios["growth_rate_factor"]
|
| 307 |
-
|
| 308 |
-
# Base scenario
|
| 309 |
-
base_scenario = self.run_scenario(
|
| 310 |
-
area, [dbh_min, dbh_max], [height_min, height_max], growth_factor
|
| 311 |
-
)
|
| 312 |
-
scenarios.append({
|
| 313 |
-
"Scenario": "Base",
|
| 314 |
-
"DBH Range": f"{dbh_min:.1f}-{dbh_max:.1f}",
|
| 315 |
-
"Height Range": f"{height_min:.1f}-{height_max:.1f}",
|
| 316 |
-
"Growth Rate": f"{growth_factor:.1f}x",
|
| 317 |
-
**base_scenario
|
| 318 |
-
})
|
| 319 |
-
|
| 320 |
-
# Low growth scenario
|
| 321 |
-
low_scenario = self.run_scenario(
|
| 322 |
-
area,
|
| 323 |
-
[dbh_min * 0.5, dbh_max * 0.5],
|
| 324 |
-
[height_min * 0.5, height_max * 0.5],
|
| 325 |
-
growth_factor
|
| 326 |
-
)
|
| 327 |
-
scenarios.append({
|
| 328 |
-
"Scenario": "Low Growth",
|
| 329 |
-
"DBH Range": f"{dbh_min*0.5:.1f}-{dbh_max*0.5:.1f}",
|
| 330 |
-
"Height Range": f"{height_min*0.5:.1f}-{height_max*0.5:.1f}",
|
| 331 |
-
"Growth Rate": f"{growth_factor:.1f}x",
|
| 332 |
-
**low_scenario
|
| 333 |
-
})
|
| 334 |
-
|
| 335 |
-
# High growth scenario
|
| 336 |
-
high_scenario = self.run_scenario(
|
| 337 |
-
area,
|
| 338 |
-
[dbh_min * 1.5, dbh_max * 1.5],
|
| 339 |
-
[height_min * 1.5, height_max * 1.5],
|
| 340 |
-
growth_factor
|
| 341 |
-
)
|
| 342 |
-
scenarios.append({
|
| 343 |
-
"Scenario": "High Growth",
|
| 344 |
-
"DBH Range": f"{dbh_min*1.5:.1f}-{dbh_max*1.5:.1f}",
|
| 345 |
-
"Height Range": f"{height_min*1.5:.1f}-{height_max*1.5:.1f}",
|
| 346 |
-
"Growth Rate": f"{growth_factor:.1f}x",
|
| 347 |
-
**high_scenario
|
| 348 |
-
})
|
| 349 |
-
|
| 350 |
-
self.scenario_results = pd.DataFrame(scenarios)
|
| 351 |
-
|
| 352 |
-
return self.results, self.species_results, self.scenario_results
|
| 353 |
|
| 354 |
def save_results(self, output_path: Path) -> None:
|
| 355 |
"""
|
|
@@ -360,4 +310,56 @@ class ERModel:
|
|
| 360 |
"""
|
| 361 |
if self.results is None:
|
| 362 |
raise ValueError("No results available. Run the model first.")
|
| 363 |
-
self.results.to_csv(output_path, index=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import numpy as np
|
| 9 |
import pandas as pd
|
| 10 |
import yaml
|
| 11 |
+
import warnings
|
| 12 |
|
| 13 |
from .allometry import calculate_biomass
|
| 14 |
from .metrics import calculate_carbon
|
|
|
|
| 19 |
"""Species-specific parameters for growth and carbon calculations."""
|
| 20 |
name: str
|
| 21 |
planting_density: float
|
| 22 |
+
# Old style
|
| 23 |
+
mortality_rates: Optional[Dict[str, float]] = None
|
| 24 |
+
# New style
|
| 25 |
+
m_ref: Optional[float] = None
|
| 26 |
+
DBH_ref: Optional[float] = None
|
| 27 |
+
p: Optional[float] = None
|
| 28 |
+
chapman_richards: Dict[str, Dict[str, float]] = None
|
| 29 |
+
allometry: Dict[str, float] = None
|
| 30 |
+
initial_values: Dict[str, float] = None
|
| 31 |
+
linear: Optional[Dict[str, Dict[str, float]]] = None
|
| 32 |
+
linear_plateau: Optional[Dict[str, Dict[str, float]]] = None
|
| 33 |
+
declining_increment: Optional[Dict[str, Dict[str, float]]] = None
|
| 34 |
|
| 35 |
|
| 36 |
@dataclass
|
|
|
|
| 85 |
self.species_results: Optional[pd.DataFrame] = None
|
| 86 |
self.scenario_results: Optional[pd.DataFrame] = None
|
| 87 |
|
| 88 |
+
self.growth_model = config.get('growth_model', 'chapman_richards')
|
| 89 |
+
|
| 90 |
+
def calculate_cohort_surviving_trees(self, planting_year: int, current_year: int, initial_trees: float, species: Species, plateau_density: Optional[float] = None, growth_model: str = None) -> float:
|
| 91 |
"""
|
| 92 |
Calculate surviving trees for a cohort planted in planting_year, in current_year.
|
| 93 |
+
Uses either per-year mortality (from config) or DBH-dependent mortality.
|
| 94 |
+
growth_model: which growth model to use for DBH (e.g., 'linear', 'linear_plateau', etc.)
|
| 95 |
"""
|
| 96 |
+
if growth_model is None:
|
| 97 |
+
growth_model = getattr(self, 'growth_model', 'chapman_richards')
|
| 98 |
age = current_year - planting_year + 1
|
| 99 |
if age < 1:
|
| 100 |
return 0
|
| 101 |
+
if initial_trees == 0:
|
| 102 |
+
# Suppress debug output for zero-initial-trees cohorts
|
| 103 |
+
return 0
|
| 104 |
+
N_live = initial_trees
|
| 105 |
+
for year in range(1, age + 1):
|
| 106 |
+
debug_info = {
|
| 107 |
+
'planting_year': planting_year,
|
| 108 |
+
'current_year': current_year,
|
| 109 |
+
'cohort_age': year,
|
| 110 |
+
'initial_trees': initial_trees,
|
| 111 |
+
'N_live_before': N_live
|
| 112 |
+
}
|
| 113 |
+
if species.mortality_rates is not None:
|
| 114 |
+
year_key = f"year_{year}"
|
| 115 |
+
if year_key in species.mortality_rates:
|
| 116 |
+
mort_rate = species.mortality_rates[year_key]
|
| 117 |
+
else:
|
| 118 |
+
mort_rate = species.mortality_rates.get("subsequent", 0)
|
| 119 |
+
print(f"[DEBUG] Year key '{year_key}' not found in mortality_rates for {species.name}. Using 'subsequent' or 0. Available keys: {list(species.mortality_rates.keys())}")
|
| 120 |
+
m = mort_rate / 100.0
|
| 121 |
+
debug_info['mortality_logic'] = 'annual'
|
| 122 |
+
debug_info['mortality_rate_percent'] = mort_rate
|
| 123 |
+
else:
|
| 124 |
+
growth_func, dbh_params = self.get_growth_function_and_params(species, growth_model, 'dbh')
|
| 125 |
+
dbh = growth_func(year, dbh_params, species.initial_values["dbh"])
|
| 126 |
+
dbh = max(dbh, 1.0)
|
| 127 |
+
m_ref = species.m_ref if species.m_ref is not None else 0.16
|
| 128 |
+
DBH_ref = species.DBH_ref if species.DBH_ref is not None else 9.0
|
| 129 |
+
p = species.p if species.p is not None else 1.493
|
| 130 |
+
m = m_ref * (DBH_ref / dbh) ** p
|
| 131 |
+
m = min(max(m, 0), 0.99)
|
| 132 |
+
debug_info['mortality_logic'] = 'dbh-dependent'
|
| 133 |
+
debug_info['mortality_rate_percent'] = m * 100
|
| 134 |
+
debug_info['dbh'] = dbh
|
| 135 |
+
N_live = N_live * (1 - m)
|
| 136 |
+
debug_info['N_live_after'] = N_live
|
| 137 |
+
print(f"[DEBUG] {species.name} | PlantingYear: {planting_year} | Year: {current_year} | CohortAge: {year} | MortalityLogic: {debug_info['mortality_logic']} | MortalityRate(%): {debug_info['mortality_rate_percent']} | N_live_before: {debug_info['N_live_before']} | N_live_after: {debug_info['N_live_after']}")
|
| 138 |
+
return N_live
|
| 139 |
|
| 140 |
def calculate_total_surviving_trees(self, year: int) -> Dict[str, float]:
|
| 141 |
"""
|
| 142 |
Calculate total surviving trees for each species in a given year, summing across all cohorts.
|
| 143 |
Returns a dict: {species_name: total_surviving_trees}
|
| 144 |
"""
|
| 145 |
+
growth_model = getattr(self, 'growth_model', 'chapman_richards')
|
| 146 |
totals = {}
|
| 147 |
for species in self.species:
|
| 148 |
total = 0
|
| 149 |
for planting_year, area in self.project.planting_schedule.items():
|
| 150 |
py = int(planting_year.split("_")[1])
|
| 151 |
initial_trees = species.planting_density * area
|
| 152 |
+
# Use plateau_density as the year-5 value for this cohort
|
| 153 |
+
plateau_density = species.planting_density * area if 5 <= (year - py + 1) else None
|
| 154 |
+
total += self.calculate_cohort_surviving_trees(py, year, initial_trees, species, plateau_density, growth_model)
|
| 155 |
totals[species.name] = total
|
| 156 |
return totals
|
| 157 |
|
|
|
|
| 166 |
total += area
|
| 167 |
return total
|
| 168 |
|
| 169 |
+
@staticmethod
|
| 170 |
+
def chapman_richards_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
a, b, c = params["a"], params["b"], params["c"]
|
| 172 |
return initial_value + (a - initial_value) * (1 - np.exp(-b * age)) ** c
|
| 173 |
|
| 174 |
+
@staticmethod
|
| 175 |
+
def linear_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
|
| 176 |
+
r = params["r"]
|
| 177 |
+
return initial_value + r * age
|
| 178 |
+
|
| 179 |
+
@staticmethod
|
| 180 |
+
def linear_plateau_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
|
| 181 |
+
r = params["r"]
|
| 182 |
+
T_p = params["T_p"]
|
| 183 |
+
a = params["a"]
|
| 184 |
+
if age <= T_p:
|
| 185 |
+
return initial_value + r * age
|
| 186 |
+
else:
|
| 187 |
+
return initial_value + a
|
| 188 |
+
|
| 189 |
+
@staticmethod
|
| 190 |
+
def declining_increment_growth(age: float, params: Dict[str, float], initial_value: float) -> float:
|
| 191 |
+
r0 = params["r0"]
|
| 192 |
+
T_m = params["T_m"]
|
| 193 |
+
# Accumulate annual increments, never negative
|
| 194 |
+
total = initial_value
|
| 195 |
+
for i in range(1, int(np.floor(age)) + 1):
|
| 196 |
+
increment = r0 * max(0, 1 - (i - 1) / T_m)
|
| 197 |
+
total += increment
|
| 198 |
+
# If age is fractional, add partial increment for the last year
|
| 199 |
+
frac = age - int(np.floor(age))
|
| 200 |
+
if frac > 0:
|
| 201 |
+
i = int(np.floor(age)) + 1
|
| 202 |
+
increment = r0 * max(0, 1 - (i - 1) / T_m)
|
| 203 |
+
total += frac * increment
|
| 204 |
+
return total
|
| 205 |
+
|
| 206 |
def calculate_carbon_for_species(self, species: Species, age: int, area: float) -> float:
|
| 207 |
"""
|
| 208 |
Calculate carbon sequestration for a single species and age.
|
|
|
|
| 213 |
Returns:
|
| 214 |
Carbon sequestration in tCO2
|
| 215 |
"""
|
| 216 |
+
# Calculate surviving trees using DBH-dependent mortality
|
| 217 |
initial_trees = species.planting_density * area
|
| 218 |
+
plateau_density = species.planting_density * area if age >= 5 else None
|
| 219 |
+
surviving = self.calculate_cohort_surviving_trees(1, age, initial_trees, species, plateau_density, self.growth_model)
|
| 220 |
+
# Calculate DBH and height using the selected growth model
|
| 221 |
+
dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
|
| 222 |
+
height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
|
| 223 |
+
dbh = dbh_func(age, dbh_params, species.initial_values["dbh"])
|
| 224 |
+
height = height_func(age, height_params, species.initial_values["height"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
# Calculate biomass using both DBH and height
|
| 226 |
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
| 227 |
# Convert to carbon
|
|
|
|
| 232 |
)
|
| 233 |
return carbon
|
| 234 |
|
| 235 |
+
def run(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
"""
|
| 237 |
Execute the full ER calculation pipeline.
|
|
|
|
| 238 |
Returns:
|
| 239 |
+
Tuple of (yearly results DataFrame, species results DataFrame)
|
| 240 |
"""
|
| 241 |
years = range(1, self.project.duration_years + 1)
|
| 242 |
results = []
|
|
|
|
| 254 |
for planting_year, area in self.project.planting_schedule.items():
|
| 255 |
py = int(planting_year.split("_")[1])
|
| 256 |
initial_trees = species.planting_density * area
|
| 257 |
+
# Use plateau_density as the year-5 value for this cohort
|
| 258 |
+
plateau_density = species.planting_density * area if 5 <= (year - py + 1) else None
|
| 259 |
+
surviving = self.calculate_cohort_surviving_trees(py, year, initial_trees, species, plateau_density, self.growth_model)
|
| 260 |
+
# Calculate DBH and height using the selected growth model
|
| 261 |
+
dbh_func, dbh_params = self.get_growth_function_and_params(species, self.growth_model, 'dbh')
|
| 262 |
+
height_func, height_params = self.get_growth_function_and_params(species, self.growth_model, 'height')
|
| 263 |
+
dbh = dbh_func(year, dbh_params, species.initial_values["dbh"])
|
| 264 |
+
height = height_func(year, height_params, species.initial_values["height"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
# Calculate biomass using both DBH and height
|
| 266 |
biomass = calculate_biomass(dbh, height, species.name, species.allometry)
|
| 267 |
# Convert to carbon
|
|
|
|
| 299 |
self.results = pd.DataFrame(results)
|
| 300 |
self.species_results = pd.DataFrame(species_results)
|
| 301 |
|
| 302 |
+
return self.results, self.species_results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
def save_results(self, output_path: Path) -> None:
|
| 305 |
"""
|
|
|
|
| 310 |
"""
|
| 311 |
if self.results is None:
|
| 312 |
raise ValueError("No results available. Run the model first.")
|
| 313 |
+
self.results.to_csv(output_path, index=False)
|
| 314 |
+
|
| 315 |
+
def get_growth_function_and_params(self, species, growth_model, dim):
|
| 316 |
+
"""
|
| 317 |
+
Returns the correct growth function and parameter dict for the given species and dimension (dbh or height).
|
| 318 |
+
"""
|
| 319 |
+
if growth_model == "linear":
|
| 320 |
+
func = ERModel.linear_growth
|
| 321 |
+
params = species.linear[dim]
|
| 322 |
+
elif growth_model == "linear_plateau":
|
| 323 |
+
func = ERModel.linear_plateau_growth
|
| 324 |
+
params = species.linear_plateau[dim]
|
| 325 |
+
elif growth_model == "declining_increment":
|
| 326 |
+
func = ERModel.declining_increment_growth
|
| 327 |
+
params = species.declining_increment[dim]
|
| 328 |
+
else:
|
| 329 |
+
func = ERModel.chapman_richards_growth
|
| 330 |
+
params = species.chapman_richards[dim]
|
| 331 |
+
return func, params
|
| 332 |
+
|
| 333 |
+
# --- Parameter sweep/test for plausible survival curves ---
|
| 334 |
+
def test_dbh_mortality_sweep():
|
| 335 |
+
import matplotlib.pyplot as plt
|
| 336 |
+
import numpy as np
|
| 337 |
+
m_refs = [0.01, 0.05, 0.1, 0.16]
|
| 338 |
+
ps = [1.0, 1.5, 2.0]
|
| 339 |
+
DBH_ref = 9.0
|
| 340 |
+
years = np.arange(1, 31)
|
| 341 |
+
initial_trees = 1000
|
| 342 |
+
results = {}
|
| 343 |
+
for m_ref in m_refs:
|
| 344 |
+
for p in ps:
|
| 345 |
+
N_live = initial_trees
|
| 346 |
+
N_lives = []
|
| 347 |
+
for year in years:
|
| 348 |
+
dbh = 1.0 + (year - 1) * 0.5 # simple linear DBH growth for test
|
| 349 |
+
dbh = max(dbh, 1.0)
|
| 350 |
+
m = m_ref * (DBH_ref / dbh) ** p
|
| 351 |
+
m = min(max(m, 0), 0.99)
|
| 352 |
+
N_live = N_live * (1 - m)
|
| 353 |
+
N_lives.append(N_live)
|
| 354 |
+
results[(m_ref, p)] = N_lives
|
| 355 |
+
plt.figure(figsize=(10,6))
|
| 356 |
+
for (m_ref, p), N_lives in results.items():
|
| 357 |
+
plt.plot(years, N_lives, label=f"m_ref={m_ref}, p={p}")
|
| 358 |
+
plt.xlabel("Year")
|
| 359 |
+
plt.ylabel("Surviving Trees")
|
| 360 |
+
plt.title("DBH-dependent Mortality Parameter Sweep")
|
| 361 |
+
plt.legend()
|
| 362 |
+
plt.grid(True)
|
| 363 |
+
plt.show()
|
| 364 |
+
|
| 365 |
+
# To run the test, call test_dbh_mortality_sweep() from __main__ or a notebook.
|
tests/test_er_model.py
CHANGED
|
@@ -20,20 +20,18 @@ def sample_config():
|
|
| 20 |
{
|
| 21 |
"name": "Test Species",
|
| 22 |
"planting_density": 1000,
|
| 23 |
-
"
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
"subsequent": 2
|
| 27 |
-
},
|
| 28 |
"chapman_richards": {
|
| 29 |
-
"a": 30,
|
| 30 |
-
"b": 0.
|
| 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": {
|
|
@@ -71,18 +69,16 @@ def test_model_initialization(config_file):
|
|
| 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 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 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):
|
|
@@ -132,4 +128,42 @@ def test_full_pipeline(config_file):
|
|
| 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"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
{
|
| 21 |
"name": "Test Species",
|
| 22 |
"planting_density": 1000,
|
| 23 |
+
"m_ref": 0.16,
|
| 24 |
+
"DBH_ref": 9.0,
|
| 25 |
+
"p": 1.493,
|
|
|
|
|
|
|
| 26 |
"chapman_richards": {
|
| 27 |
+
"dbh": {"a": 30, "b": 0.15, "c": 1.5},
|
| 28 |
+
"height": {"a": 10, "b": 0.05, "c": 1.2}
|
|
|
|
| 29 |
},
|
| 30 |
"allometry": {
|
| 31 |
"biomass_equation": "0.2 * (dbh ** 2.4)",
|
| 32 |
"root_shoot_ratio": 0.4
|
| 33 |
+
},
|
| 34 |
+
"initial_values": {"dbh": 1.0, "height": 0.5}
|
| 35 |
}
|
| 36 |
],
|
| 37 |
"project": {
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def test_surviving_trees_calculation(config_file):
|
| 72 |
+
"""Test tree survival calculations with DBH-dependent mortality."""
|
| 73 |
model = ERModel(config_file)
|
| 74 |
species = model.species[0]
|
| 75 |
+
# Test first year mortality (should use DBH-dependent formula)
|
| 76 |
+
dbh = 9.0
|
| 77 |
+
m = species.m_ref * (species.DBH_ref / dbh) ** species.p
|
| 78 |
+
initial_trees = 1000
|
| 79 |
+
N_live = initial_trees * (1 - m)
|
| 80 |
+
expected = initial_trees * (1 - 0.16)
|
| 81 |
+
assert np.isclose(N_live, expected, atol=1e-3)
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def test_chapman_richards_growth(config_file):
|
|
|
|
| 128 |
assert "year" in results.columns
|
| 129 |
assert "gross_carbon" in results.columns
|
| 130 |
assert "net_carbon" in results.columns
|
| 131 |
+
assert all(results["net_carbon"] <= results["gross_carbon"])
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def test_dbh_dependent_mortality():
|
| 135 |
+
"""Test that DBH-dependent mortality matches expected at DBH=9 and year-5 plateau."""
|
| 136 |
+
from src.er_model import ERModel, Species
|
| 137 |
+
# Minimal species config
|
| 138 |
+
species = Species(
|
| 139 |
+
name="Test Species",
|
| 140 |
+
planting_density=1000,
|
| 141 |
+
m_ref=0.16,
|
| 142 |
+
DBH_ref=9.0,
|
| 143 |
+
p=1.493,
|
| 144 |
+
chapman_richards={
|
| 145 |
+
"dbh": {"a": 9.0, "b": 0.5, "c": 1.0},
|
| 146 |
+
"height": {"a": 5.0, "b": 0.2, "c": 1.0}
|
| 147 |
+
},
|
| 148 |
+
allometry={"equation": "0.2 * (dbh ** 2.4)", "root_shoot_ratio": 0.4},
|
| 149 |
+
initial_values={"dbh": 9.0, "height": 5.0}
|
| 150 |
+
)
|
| 151 |
+
# At DBH = 9, mortality = 0.16
|
| 152 |
+
dbh = 9.0
|
| 153 |
+
m = species.m_ref * (species.DBH_ref / dbh) ** species.p
|
| 154 |
+
assert np.isclose(m, 0.16, atol=1e-3)
|
| 155 |
+
|
| 156 |
+
# Simulate 5 years, enforce plateau at year 5
|
| 157 |
+
initial_trees = 1000
|
| 158 |
+
plateau_density = 2000
|
| 159 |
+
class DummyModel:
|
| 160 |
+
def chapman_richards_growth(self, age, params, initial):
|
| 161 |
+
return dbh # Always 9 for this test
|
| 162 |
+
model = DummyModel()
|
| 163 |
+
N_live = initial_trees
|
| 164 |
+
for y in range(1, 6):
|
| 165 |
+
m = species.m_ref * (species.DBH_ref / dbh) ** species.p
|
| 166 |
+
N_live = N_live * (1 - m)
|
| 167 |
+
if y == 5:
|
| 168 |
+
N_live = plateau_density
|
| 169 |
+
assert N_live == plateau_density
|