strategicc 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strategicc-2.2.0/LICENSE +21 -0
- strategicc-2.2.0/PKG-INFO +123 -0
- strategicc-2.2.0/README.md +75 -0
- strategicc-2.2.0/pyproject.toml +44 -0
- strategicc-2.2.0/setup.cfg +4 -0
- strategicc-2.2.0/strategicc/__init__.py +9 -0
- strategicc-2.2.0/strategicc/accounting/__init__.py +12 -0
- strategicc-2.2.0/strategicc/accounting/csv_loader.py +113 -0
- strategicc-2.2.0/strategicc/accounting/outputs.py +260 -0
- strategicc-2.2.0/strategicc/accounting/seea.py +295 -0
- strategicc-2.2.0/strategicc/config.py +62 -0
- strategicc-2.2.0/strategicc/core/__init__.py +13 -0
- strategicc-2.2.0/strategicc/core/adjacency.py +77 -0
- strategicc-2.2.0/strategicc/core/multipliers.py +78 -0
- strategicc-2.2.0/strategicc/core/spatial.py +103 -0
- strategicc-2.2.0/strategicc/core/transitions.py +83 -0
- strategicc-2.2.0/strategicc/engine.py +410 -0
- strategicc-2.2.0/strategicc/io/__init__.py +13 -0
- strategicc-2.2.0/strategicc/io/csv_loader.py +270 -0
- strategicc-2.2.0/strategicc/io/raster.py +124 -0
- strategicc-2.2.0/strategicc/outputs.py +591 -0
- strategicc-2.2.0/strategicc/run.py +137 -0
- strategicc-2.2.0/strategicc.egg-info/PKG-INFO +123 -0
- strategicc-2.2.0/strategicc.egg-info/SOURCES.txt +27 -0
- strategicc-2.2.0/strategicc.egg-info/dependency_links.txt +1 -0
- strategicc-2.2.0/strategicc.egg-info/entry_points.txt +2 -0
- strategicc-2.2.0/strategicc.egg-info/requires.txt +8 -0
- strategicc-2.2.0/strategicc.egg-info/top_level.txt +1 -0
- strategicc-2.2.0/tests/test_accounting.py +168 -0
strategicc-2.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shlhnj
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strategicc
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: State and Transition Integrated Economic-Environmental Accounting
|
|
5
|
+
Author: Shlhnj
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Shlhnj
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/Shlhnj/strategicc
|
|
29
|
+
Project-URL: Repository, https://github.com/Shlhnj/strategicc
|
|
30
|
+
Project-URL: Issues, https://github.com/Shlhnj/strategicc/issues
|
|
31
|
+
Keywords: STSM,land cover,ecosystem accounting,SEEA-EA,landscape ecology,Monte Carlo
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
36
|
+
Classifier: Intended Audience :: Science/Research
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: numpy>=1.24
|
|
41
|
+
Requires-Dist: pandas>=2.0
|
|
42
|
+
Requires-Dist: Pillow>=10.0
|
|
43
|
+
Requires-Dist: matplotlib>=3.7
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# strategicc
|
|
50
|
+
STRATEGICC: State and Transition Integrated Economic-Environmental Accounting
|
|
51
|
+
|
|
52
|
+
A python package implementation of State-and-Transition Simulation Model (STSM) framework laid by Daniel et al (2016) (https://doi.org/10.1111/2041-210X.12597), integrated with the System of Economic-Environmental Accounting - Ecosystem Accounting (SEEA-EA) by United Nations (https://seea.un.org/ecosystem-accounting).
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
### install the package
|
|
56
|
+
```
|
|
57
|
+
!pip install git+https://github.com/Shlhnj/strategicc.git
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Set Configuration
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
import strategicc.config as cfg
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
|
|
67
|
+
cfg.LULC_PATH = Path("2022.tif")
|
|
68
|
+
cfg.STATE_CLASSES_CSV = Path("25062026 State Class.csv")
|
|
69
|
+
cfg.TRANSITIONS_CSV = Path("23062026_stsm_transition_probabilities.csv")
|
|
70
|
+
cfg.SPATIAL_MULT_CSV = Path("27062026 Transition Spatial Multipliers.csv")
|
|
71
|
+
cfg.TRANSITION_MULT_CSV= Path("27062026 Transition Multipliers.csv")
|
|
72
|
+
cfg.MULT_DIR = Path("mult_spat/") #folder for transition spatial multiplier
|
|
73
|
+
cfg.OUT_DIR = Path("strategicc_test") #output folder
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
cfg.ADJACENCY_STRENGTH = 2
|
|
77
|
+
cfg.START_YEAR = 2022
|
|
78
|
+
cfg.N_TIMESTEPS = 30
|
|
79
|
+
cfg.N_ITERATIONS = 100
|
|
80
|
+
cfg.RNG_SEED = 42
|
|
81
|
+
|
|
82
|
+
cfg.USE_ADJACENCY = True
|
|
83
|
+
cfg.USE_SPATIAL_MULT = True
|
|
84
|
+
cfg.USE_TRANS_MULTIPLIER = True
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Diagnose Configuration
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
from strategicc import STSMEngine
|
|
91
|
+
|
|
92
|
+
engine = STSMEngine.from_config()
|
|
93
|
+
engine.load()
|
|
94
|
+
engine.diagnostic()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Run Engine
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
engine.run()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Show Summary Plot
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
from strategicc import outputs
|
|
107
|
+
|
|
108
|
+
summary_dir = engine.out_dir / "summary"
|
|
109
|
+
|
|
110
|
+
print("Building summary tables...")
|
|
111
|
+
area_df, trans_df = outputs.build_summary_tables(engine.iter_dirs, summary_dir)
|
|
112
|
+
|
|
113
|
+
print("Plotting area envelope...")
|
|
114
|
+
outputs.plot_area_envelope(area_df, engine.classes, summary_dir)
|
|
115
|
+
|
|
116
|
+
print("Plotting transition envelope...")
|
|
117
|
+
outputs.plot_transition_envelope(trans_df, summary_dir)
|
|
118
|
+
|
|
119
|
+
Show plots inline
|
|
120
|
+
from IPython.display import Image, display
|
|
121
|
+
display(Image(str(summary_dir / "area_envelope.png")))
|
|
122
|
+
display(Image(str(summary_dir / "transition_envelope.png")))
|
|
123
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# strategicc
|
|
2
|
+
STRATEGICC: State and Transition Integrated Economic-Environmental Accounting
|
|
3
|
+
|
|
4
|
+
A python package implementation of State-and-Transition Simulation Model (STSM) framework laid by Daniel et al (2016) (https://doi.org/10.1111/2041-210X.12597), integrated with the System of Economic-Environmental Accounting - Ecosystem Accounting (SEEA-EA) by United Nations (https://seea.un.org/ecosystem-accounting).
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
### install the package
|
|
8
|
+
```
|
|
9
|
+
!pip install git+https://github.com/Shlhnj/strategicc.git
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Set Configuration
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
import strategicc.config as cfg
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
cfg.LULC_PATH = Path("2022.tif")
|
|
20
|
+
cfg.STATE_CLASSES_CSV = Path("25062026 State Class.csv")
|
|
21
|
+
cfg.TRANSITIONS_CSV = Path("23062026_stsm_transition_probabilities.csv")
|
|
22
|
+
cfg.SPATIAL_MULT_CSV = Path("27062026 Transition Spatial Multipliers.csv")
|
|
23
|
+
cfg.TRANSITION_MULT_CSV= Path("27062026 Transition Multipliers.csv")
|
|
24
|
+
cfg.MULT_DIR = Path("mult_spat/") #folder for transition spatial multiplier
|
|
25
|
+
cfg.OUT_DIR = Path("strategicc_test") #output folder
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
cfg.ADJACENCY_STRENGTH = 2
|
|
29
|
+
cfg.START_YEAR = 2022
|
|
30
|
+
cfg.N_TIMESTEPS = 30
|
|
31
|
+
cfg.N_ITERATIONS = 100
|
|
32
|
+
cfg.RNG_SEED = 42
|
|
33
|
+
|
|
34
|
+
cfg.USE_ADJACENCY = True
|
|
35
|
+
cfg.USE_SPATIAL_MULT = True
|
|
36
|
+
cfg.USE_TRANS_MULTIPLIER = True
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Diagnose Configuration
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
from strategicc import STSMEngine
|
|
43
|
+
|
|
44
|
+
engine = STSMEngine.from_config()
|
|
45
|
+
engine.load()
|
|
46
|
+
engine.diagnostic()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Run Engine
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
engine.run()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Show Summary Plot
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
from strategicc import outputs
|
|
59
|
+
|
|
60
|
+
summary_dir = engine.out_dir / "summary"
|
|
61
|
+
|
|
62
|
+
print("Building summary tables...")
|
|
63
|
+
area_df, trans_df = outputs.build_summary_tables(engine.iter_dirs, summary_dir)
|
|
64
|
+
|
|
65
|
+
print("Plotting area envelope...")
|
|
66
|
+
outputs.plot_area_envelope(area_df, engine.classes, summary_dir)
|
|
67
|
+
|
|
68
|
+
print("Plotting transition envelope...")
|
|
69
|
+
outputs.plot_transition_envelope(trans_df, summary_dir)
|
|
70
|
+
|
|
71
|
+
Show plots inline
|
|
72
|
+
from IPython.display import Image, display
|
|
73
|
+
display(Image(str(summary_dir / "area_envelope.png")))
|
|
74
|
+
display(Image(str(summary_dir / "transition_envelope.png")))
|
|
75
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strategicc"
|
|
7
|
+
version = "2.2.0"
|
|
8
|
+
description = "State and Transition Integrated Economic-Environmental Accounting"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Shlhnj" }]
|
|
12
|
+
keywords = [
|
|
13
|
+
"STSM", "land cover", "ecosystem accounting",
|
|
14
|
+
"SEEA-EA", "landscape ecology", "Monte Carlo",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.10"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.24",
|
|
26
|
+
"pandas>=2.0",
|
|
27
|
+
"Pillow>=10.0",
|
|
28
|
+
"matplotlib>=3.7",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=7.0", "pytest-cov"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/Shlhnj/strategicc"
|
|
36
|
+
Repository = "https://github.com/Shlhnj/strategicc"
|
|
37
|
+
Issues = "https://github.com/Shlhnj/strategicc/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
strategicc = "strategicc.run:main"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["."]
|
|
44
|
+
include = ["strategicc*"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
strategicc/accounting — SEEA-EA accounting module v2.0
|
|
3
|
+
----------------------------------------------------------
|
|
4
|
+
Produces ecosystem extent, physical flow, monetary flow,
|
|
5
|
+
transition matrix, and change-in-value accounts from
|
|
6
|
+
simulation outputs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .seea import SEEAAccount
|
|
10
|
+
from .csv_loader import load_ecosystem_services, EcosystemService
|
|
11
|
+
|
|
12
|
+
__all__ = ["SEEAAccount", "load_ecosystem_services", "EcosystemService"]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
strategicc/accounting/csv_loader.py
|
|
3
|
+
-------------------------------------
|
|
4
|
+
Parse EcosystemServices.csv into EcosystemService dataclasses.
|
|
5
|
+
|
|
6
|
+
CSV format (two modes supported):
|
|
7
|
+
|
|
8
|
+
Mode A — monetary value per ha only (no physical unit):
|
|
9
|
+
StateClassId, ServiceName, ServiceType, ValuePerHa, Currency
|
|
10
|
+
Mangrove, Ecotourism, Cultural, 12500000, IDR
|
|
11
|
+
|
|
12
|
+
Mode B — physical unit + monetary value per ha:
|
|
13
|
+
StateClassId, ServiceName, ServiceType, ValuePerHa, Currency, PhysicalUnit, PhysicalValuePerHa
|
|
14
|
+
Mangrove, Carbon Sequestration, Regulating, 97500000, IDR, MgC/ha, 1300
|
|
15
|
+
|
|
16
|
+
ServiceType must be one of: Provisioning, Regulating, Cultural
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
import csv
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
VALID_SERVICE_TYPES = {"Provisioning", "Regulating", "Cultural"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class EcosystemService:
|
|
30
|
+
"""One ecosystem service entry from EcosystemServices.csv."""
|
|
31
|
+
state_class: str # matches StateClass name e.g. "Mangrove"
|
|
32
|
+
service_name: str # e.g. "Carbon Sequestration"
|
|
33
|
+
service_type: str # "Provisioning" | "Regulating" | "Cultural"
|
|
34
|
+
value_per_ha: float # monetary value per hectare per year
|
|
35
|
+
currency: str # e.g. "IDR"
|
|
36
|
+
physical_unit: str | None # e.g. "MgC/ha" — None if Mode A
|
|
37
|
+
physical_per_ha: float | None # physical quantity per ha — None if Mode A
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def has_physical(self) -> bool:
|
|
41
|
+
return self.physical_unit is not None and self.physical_per_ha is not None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_ecosystem_services(path: str | Path) -> list[EcosystemService]:
|
|
45
|
+
"""
|
|
46
|
+
Parse EcosystemServices.csv.
|
|
47
|
+
|
|
48
|
+
Required columns:
|
|
49
|
+
StateClassId, ServiceName, ServiceType, ValuePerHa, Currency
|
|
50
|
+
|
|
51
|
+
Optional columns (Mode B):
|
|
52
|
+
PhysicalUnit, PhysicalValuePerHa
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
list of EcosystemService
|
|
57
|
+
"""
|
|
58
|
+
path = Path(path)
|
|
59
|
+
services: list[EcosystemService] = []
|
|
60
|
+
|
|
61
|
+
with path.open(newline="", encoding="utf-8-sig") as fh:
|
|
62
|
+
reader = csv.DictReader(fh)
|
|
63
|
+
for i, row in enumerate(reader, start=2):
|
|
64
|
+
state_class = row.get("StateClassId", "").strip()
|
|
65
|
+
service_name = row.get("ServiceName", "").strip()
|
|
66
|
+
service_type = row.get("ServiceType", "").strip()
|
|
67
|
+
currency = row.get("Currency", "").strip()
|
|
68
|
+
|
|
69
|
+
# Parse monetary value
|
|
70
|
+
try:
|
|
71
|
+
value_per_ha = float(row.get("ValuePerHa", "").strip())
|
|
72
|
+
except (ValueError, AttributeError):
|
|
73
|
+
print(f" [Warning] Row {i}: invalid ValuePerHa — skipped")
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Validate service type
|
|
77
|
+
if service_type not in VALID_SERVICE_TYPES:
|
|
78
|
+
print(f" [Warning] Row {i}: unknown ServiceType '{service_type}' "
|
|
79
|
+
f"— must be one of {VALID_SERVICE_TYPES}")
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if not state_class or not service_name:
|
|
83
|
+
print(f" [Warning] Row {i}: missing StateClassId or ServiceName — skipped")
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
# Optional physical columns (Mode B)
|
|
87
|
+
phys_unit = row.get("PhysicalUnit", "").strip() or None
|
|
88
|
+
phys_raw = row.get("PhysicalValuePerHa", "").strip()
|
|
89
|
+
try:
|
|
90
|
+
phys_per_ha = float(phys_raw) if phys_raw else None
|
|
91
|
+
except ValueError:
|
|
92
|
+
phys_per_ha = None
|
|
93
|
+
|
|
94
|
+
# Both must be present for Mode B, or both absent for Mode A
|
|
95
|
+
if (phys_unit is None) != (phys_per_ha is None):
|
|
96
|
+
print(f" [Warning] Row {i} ({state_class} / {service_name}): "
|
|
97
|
+
"PhysicalUnit and PhysicalValuePerHa must both be present "
|
|
98
|
+
"or both absent — treating as Mode A")
|
|
99
|
+
phys_unit = phys_per_ha = None
|
|
100
|
+
|
|
101
|
+
services.append(EcosystemService(
|
|
102
|
+
state_class = state_class,
|
|
103
|
+
service_name = service_name,
|
|
104
|
+
service_type = service_type,
|
|
105
|
+
value_per_ha = value_per_ha,
|
|
106
|
+
currency = currency,
|
|
107
|
+
physical_unit = phys_unit,
|
|
108
|
+
physical_per_ha = phys_per_ha,
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
print(f" {len(services)} ecosystem service entries loaded "
|
|
112
|
+
f"({sum(s.has_physical for s in services)} with physical units)")
|
|
113
|
+
return services
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
strategicc/accounting/outputs.py — SEEA-EA output functions v2.0
|
|
3
|
+
--------------------------------------------------------------------
|
|
4
|
+
Saves all ecosystem accounts as CSVs and generates plots.
|
|
5
|
+
|
|
6
|
+
Functions
|
|
7
|
+
---------
|
|
8
|
+
save_all_accounts — save all account tables to CSV
|
|
9
|
+
plot_monetary_flows — stacked area chart of total ecosystem value over time
|
|
10
|
+
plot_value_by_service — line chart per service type over time
|
|
11
|
+
plot_transition_heatmap — heatmap of transition matrix (area and value)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import matplotlib
|
|
20
|
+
matplotlib.use("Agg")
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
import matplotlib.ticker as mticker
|
|
23
|
+
|
|
24
|
+
from strategicc.io.csv_loader import StateClass
|
|
25
|
+
from strategicc.accounting.seea import SEEAAccount
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Color helpers ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def _class_colors(classes: dict[int, StateClass]) -> dict[str, tuple]:
|
|
31
|
+
return {
|
|
32
|
+
sc.name: (sc.color[1]/255, sc.color[2]/255, sc.color[3]/255)
|
|
33
|
+
for sc in classes.values()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Save all account tables ───────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
def save_all_accounts(
|
|
40
|
+
acct: SEEAAccount,
|
|
41
|
+
out_dir: Path,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Save all SEEA-EA account tables as CSVs."""
|
|
44
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
acct.extent_account().to_csv(out_dir / "seea_extent_account.csv")
|
|
47
|
+
print(f" Saved: seea_extent_account.csv")
|
|
48
|
+
|
|
49
|
+
acct.transition_matrix().to_csv(out_dir / "seea_transition_matrix_area.csv")
|
|
50
|
+
print(f" Saved: seea_transition_matrix_area.csv")
|
|
51
|
+
|
|
52
|
+
acct.value_change_matrix().to_csv(out_dir / "seea_transition_matrix_value.csv")
|
|
53
|
+
print(f" Saved: seea_transition_matrix_value.csv")
|
|
54
|
+
|
|
55
|
+
acct.monetary_flow_account().to_csv(out_dir / "seea_monetary_flow_account.csv")
|
|
56
|
+
print(f" Saved: seea_monetary_flow_account.csv")
|
|
57
|
+
|
|
58
|
+
phys = acct.physical_flow_account()
|
|
59
|
+
if phys is not None:
|
|
60
|
+
phys.to_csv(out_dir / "seea_physical_flow_account.csv")
|
|
61
|
+
print(f" Saved: seea_physical_flow_account.csv")
|
|
62
|
+
|
|
63
|
+
acct.total_value_by_class().to_csv(out_dir / "seea_total_value_by_class.csv")
|
|
64
|
+
print(f" Saved: seea_total_value_by_class.csv")
|
|
65
|
+
|
|
66
|
+
acct.change_in_value().to_csv(out_dir / "seea_change_in_value.csv")
|
|
67
|
+
print(f" Saved: seea_change_in_value.csv")
|
|
68
|
+
|
|
69
|
+
acct.uncertainty_summary().to_csv(out_dir / "seea_uncertainty_summary.csv", index=False)
|
|
70
|
+
print(f" Saved: seea_uncertainty_summary.csv")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Plot: stacked area — total ecosystem value over time ──────────────────────
|
|
74
|
+
|
|
75
|
+
def plot_monetary_flows(
|
|
76
|
+
acct: SEEAAccount,
|
|
77
|
+
classes: dict[int, StateClass],
|
|
78
|
+
out_dir: Path,
|
|
79
|
+
filename: str = "seea_monetary_flows.png",
|
|
80
|
+
) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Stacked area chart: total ecosystem service value per class over time.
|
|
83
|
+
Shows which classes contribute most to total landscape value.
|
|
84
|
+
"""
|
|
85
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
tv = acct.total_value_by_class()
|
|
87
|
+
colors = _class_colors(classes)
|
|
88
|
+
years = tv.index.tolist()
|
|
89
|
+
|
|
90
|
+
fig, axes = plt.subplots(2, 1, figsize=(12, 10))
|
|
91
|
+
|
|
92
|
+
# ── Panel 1: stacked area by class ────────────────────────────────────────
|
|
93
|
+
ax1 = axes[0]
|
|
94
|
+
bottom = np.zeros(len(years))
|
|
95
|
+
for col in tv.columns:
|
|
96
|
+
vals = tv[col].values
|
|
97
|
+
color = colors.get(col, (0.5, 0.5, 0.5))
|
|
98
|
+
ax1.fill_between(years, bottom, bottom + vals,
|
|
99
|
+
alpha=0.85, color=color, label=col)
|
|
100
|
+
bottom += vals
|
|
101
|
+
|
|
102
|
+
ax1.set_ylabel("Total ecosystem value (currency/yr)", fontsize=10)
|
|
103
|
+
ax1.set_title("Total Ecosystem Service Value by Class", fontsize=11)
|
|
104
|
+
ax1.legend(loc="upper right", fontsize=8, framealpha=0.8)
|
|
105
|
+
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(
|
|
106
|
+
lambda x, _: f"{x/1e6:.1f}M" if x >= 1e6 else f"{x:,.0f}"
|
|
107
|
+
))
|
|
108
|
+
ax1.grid(True, alpha=0.2)
|
|
109
|
+
|
|
110
|
+
# ── Panel 2: year-on-year change in total value ───────────────────────────
|
|
111
|
+
ax2 = axes[1]
|
|
112
|
+
delta = acct.change_in_value()["Total"].dropna()
|
|
113
|
+
colors_bar = ["#2ecc71" if v >= 0 else "#e74c3c" for v in delta.values]
|
|
114
|
+
ax2.bar(delta.index, delta.values, color=colors_bar, alpha=0.85, width=0.7)
|
|
115
|
+
ax2.axhline(0, color="black", linewidth=0.8)
|
|
116
|
+
ax2.set_xlabel("Year", fontsize=10)
|
|
117
|
+
ax2.set_ylabel("Change in value (currency/yr)", fontsize=10)
|
|
118
|
+
ax2.set_title("Year-on-Year Change in Total Ecosystem Value", fontsize=11)
|
|
119
|
+
ax2.yaxis.set_major_formatter(mticker.FuncFormatter(
|
|
120
|
+
lambda x, _: f"{x/1e6:.1f}M" if abs(x) >= 1e6 else f"{x:,.0f}"
|
|
121
|
+
))
|
|
122
|
+
ax2.grid(True, alpha=0.2, axis="y")
|
|
123
|
+
|
|
124
|
+
# ── Uncertainty band on panel 1 ───────────────────────────────────────────
|
|
125
|
+
unc = acct.uncertainty_summary().set_index("Year")
|
|
126
|
+
total = acct.total_value_by_class().sum(axis=1)
|
|
127
|
+
if not unc.empty:
|
|
128
|
+
ax1.fill_between(
|
|
129
|
+
unc.index, unc["Min value"], unc["Max value"],
|
|
130
|
+
alpha=0.12, color="grey", label="Min–Max range"
|
|
131
|
+
)
|
|
132
|
+
ax1.legend(loc="upper right", fontsize=8, framealpha=0.8)
|
|
133
|
+
|
|
134
|
+
plt.tight_layout()
|
|
135
|
+
out_path = out_dir / filename
|
|
136
|
+
fig.savefig(out_path, dpi=150, bbox_inches="tight")
|
|
137
|
+
plt.close(fig)
|
|
138
|
+
print(f" Saved: {out_path}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── Plot: line chart per service type ─────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
def plot_value_by_service(
|
|
144
|
+
acct: SEEAAccount,
|
|
145
|
+
out_dir: Path,
|
|
146
|
+
filename: str = "seea_value_by_service.png",
|
|
147
|
+
) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Line chart: total monetary value per service type over time.
|
|
150
|
+
One line per service (Provisioning / Regulating / Cultural).
|
|
151
|
+
"""
|
|
152
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
mf = acct.monetary_flow_account()
|
|
154
|
+
years = mf.index.tolist()
|
|
155
|
+
|
|
156
|
+
# Aggregate by service type (top level of MultiIndex columns)
|
|
157
|
+
type_totals: dict[str, list[float]] = {}
|
|
158
|
+
for col in mf.columns:
|
|
159
|
+
stype = col[0]
|
|
160
|
+
type_totals.setdefault(stype, np.zeros(len(years)))
|
|
161
|
+
type_totals[stype] += mf[col].values
|
|
162
|
+
|
|
163
|
+
type_colors = {
|
|
164
|
+
"Provisioning": "#e67e22",
|
|
165
|
+
"Regulating": "#27ae60",
|
|
166
|
+
"Cultural": "#8e44ad",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fig, ax = plt.subplots(figsize=(12, 5))
|
|
170
|
+
for stype, vals in type_totals.items():
|
|
171
|
+
color = type_colors.get(stype, "#2c3e50")
|
|
172
|
+
ax.plot(years, vals, color=color, linewidth=2.5, label=stype, zorder=3)
|
|
173
|
+
ax.fill_between(years, 0, vals, color=color, alpha=0.08, zorder=2)
|
|
174
|
+
|
|
175
|
+
# Also plot individual services as thin dashed lines
|
|
176
|
+
for col in mf.columns:
|
|
177
|
+
stype = col[0]
|
|
178
|
+
sname = col[1]
|
|
179
|
+
color = type_colors.get(stype, "#2c3e50")
|
|
180
|
+
ax.plot(years, mf[col].values, color=color,
|
|
181
|
+
linewidth=0.8, linestyle="--", alpha=0.5,
|
|
182
|
+
label=f" {sname}")
|
|
183
|
+
|
|
184
|
+
ax.set_xlabel("Year", fontsize=10)
|
|
185
|
+
ax.set_ylabel("Value (currency/yr)", fontsize=10)
|
|
186
|
+
ax.set_title("Ecosystem Service Value by Service Type", fontsize=11)
|
|
187
|
+
ax.yaxis.set_major_formatter(mticker.FuncFormatter(
|
|
188
|
+
lambda x, _: f"{x/1e6:.1f}M" if x >= 1e6 else f"{x:,.0f}"
|
|
189
|
+
))
|
|
190
|
+
ax.legend(loc="upper right", fontsize=8, framealpha=0.8, ncol=2)
|
|
191
|
+
ax.grid(True, alpha=0.2)
|
|
192
|
+
|
|
193
|
+
plt.tight_layout()
|
|
194
|
+
out_path = out_dir / filename
|
|
195
|
+
fig.savefig(out_path, dpi=150, bbox_inches="tight")
|
|
196
|
+
plt.close(fig)
|
|
197
|
+
print(f" Saved: {out_path}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ── Plot: transition heatmap ──────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
def plot_transition_heatmap(
|
|
203
|
+
acct: SEEAAccount,
|
|
204
|
+
out_dir: Path,
|
|
205
|
+
filename: str = "seea_transition_heatmap.png",
|
|
206
|
+
) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Two-panel heatmap:
|
|
209
|
+
Left — area (ha) converted between classes (transition matrix)
|
|
210
|
+
Right — monetary value change from those conversions
|
|
211
|
+
"""
|
|
212
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
tm = acct.transition_matrix()
|
|
214
|
+
vm = acct.value_change_matrix()
|
|
215
|
+
|
|
216
|
+
if tm.empty:
|
|
217
|
+
print(" [Skip] transition matrix empty — no heatmap generated")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
|
|
221
|
+
|
|
222
|
+
for ax, data, title, cmap, fmt in [
|
|
223
|
+
(axes[0], tm, "Area converted (ha)", "YlOrBr", ".1f"),
|
|
224
|
+
(axes[1], vm, "Value change (currency)", "RdYlGn", ".0f"),
|
|
225
|
+
]:
|
|
226
|
+
arr = data.values.astype(float)
|
|
227
|
+
|
|
228
|
+
# Mask diagonal (no-change cells)
|
|
229
|
+
mask_diag = np.eye(arr.shape[0], dtype=bool)
|
|
230
|
+
arr_plot = np.where(mask_diag, np.nan, arr)
|
|
231
|
+
|
|
232
|
+
vmax = np.nanmax(np.abs(arr_plot)) if not np.all(np.isnan(arr_plot)) else 1
|
|
233
|
+
vmin = -vmax if cmap == "RdYlGn" else 0
|
|
234
|
+
|
|
235
|
+
im = ax.imshow(arr_plot, cmap=cmap, vmin=vmin, vmax=vmax, aspect="auto")
|
|
236
|
+
plt.colorbar(im, ax=ax, shrink=0.8)
|
|
237
|
+
|
|
238
|
+
labels = list(data.index)
|
|
239
|
+
ax.set_xticks(range(len(labels)))
|
|
240
|
+
ax.set_yticks(range(len(labels)))
|
|
241
|
+
ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=8)
|
|
242
|
+
ax.set_yticklabels(labels, fontsize=8)
|
|
243
|
+
ax.set_xlabel("To class", fontsize=9)
|
|
244
|
+
ax.set_ylabel("From class", fontsize=9)
|
|
245
|
+
ax.set_title(title, fontsize=10)
|
|
246
|
+
|
|
247
|
+
# Annotate non-zero, non-diagonal cells
|
|
248
|
+
for i in range(arr.shape[0]):
|
|
249
|
+
for j in range(arr.shape[1]):
|
|
250
|
+
if not mask_diag[i, j] and arr[i, j] != 0:
|
|
251
|
+
ax.text(j, i, f"{arr[i,j]:{fmt}}",
|
|
252
|
+
ha="center", va="center", fontsize=7,
|
|
253
|
+
color="black")
|
|
254
|
+
|
|
255
|
+
plt.suptitle("Ecosystem Transition Matrix", fontsize=12, y=1.01)
|
|
256
|
+
plt.tight_layout()
|
|
257
|
+
out_path = out_dir / filename
|
|
258
|
+
fig.savefig(out_path, dpi=150, bbox_inches="tight")
|
|
259
|
+
plt.close(fig)
|
|
260
|
+
print(f" Saved: {out_path}")
|