hestia-earth-models 0.64.9__py3-none-any.whl → 0.64.11__py3-none-any.whl
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.
Potentially problematic release.
This version of hestia-earth-models might be problematic. Click here for more details.
- hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +175 -0
- hestia_earth/models/cml2001Baseline/abioticResourceDepletionMineralsAndMetals.py +136 -0
- hestia_earth/models/cml2001Baseline/eutrophicationPotentialExcludingFate.py +2 -2
- hestia_earth/models/cml2001Baseline/terrestrialAcidificationPotentialIncludingFateAverageEurope.py +2 -2
- hestia_earth/models/cml2001NonBaseline/eutrophicationPotentialIncludingFateAverageEurope.py +2 -2
- hestia_earth/models/cml2001NonBaseline/terrestrialAcidificationPotentialExcludingFate.py +2 -2
- hestia_earth/models/cycle/completeness/cropResidue.py +15 -10
- hestia_earth/models/cycle/completeness/freshForage.py +60 -0
- hestia_earth/models/edip2003/ozoneDepletionPotential.py +2 -2
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandTransformation.py +2 -2
- hestia_earth/models/fantkeEtAl2016/damageToHumanHealthParticulateMatterFormation.py +7 -17
- hestia_earth/models/ipcc2013ExcludingFeedbacks/gwp100.py +2 -2
- hestia_earth/models/ipcc2013IncludingFeedbacks/gwp100.py +2 -2
- hestia_earth/models/ipcc2019/aboveGroundBiomass.py +31 -243
- hestia_earth/models/ipcc2019/belowGroundBiomass.py +529 -0
- hestia_earth/models/ipcc2019/biomass_utils.py +406 -0
- hestia_earth/models/ipcc2019/{co2ToAirAboveGroundBiomassStockChangeLandUseChange.py → co2ToAirAboveGroundBiomassStockChange.py} +19 -7
- hestia_earth/models/ipcc2019/{co2ToAirBelowGroundBiomassStockChangeLandUseChange.py → co2ToAirBelowGroundBiomassStockChange.py} +19 -7
- hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +402 -73
- hestia_earth/models/ipcc2019/{co2ToAirSoilOrganicCarbonStockChangeManagementChange.py → co2ToAirSoilOrganicCarbonStockChange.py} +20 -8
- hestia_earth/models/ipcc2019/organicCarbonPerHa.py +3 -1
- hestia_earth/models/ipcc2019/pastureGrass_utils.py +6 -7
- hestia_earth/models/ipcc2021/gwp100.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsClimateChange.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthClimateChange.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsClimateChange.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthClimateChange.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
- hestia_earth/models/mocking/build_mock_search.py +44 -0
- hestia_earth/models/mocking/mock_search.py +8 -49
- hestia_earth/models/mocking/search-results.json +3055 -558
- hestia_earth/models/poschEtAl2008/terrestrialAcidificationPotentialAccumulatedExceedance.py +3 -3
- hestia_earth/models/poschEtAl2008/terrestrialEutrophicationPotentialAccumulatedExceedance.py +3 -3
- hestia_earth/models/preload_requests.py +1 -1
- hestia_earth/models/recipe2016Egalitarian/ecosystemDamageOzoneFormation.py +2 -2
- hestia_earth/models/recipe2016Egalitarian/freshwaterEutrophicationPotential.py +2 -2
- hestia_earth/models/recipe2016Egalitarian/humanDamageOzoneFormation.py +2 -2
- hestia_earth/models/recipe2016Egalitarian/marineEutrophicationPotential.py +2 -2
- hestia_earth/models/recipe2016Egalitarian/ozoneDepletionPotential.py +2 -2
- hestia_earth/models/recipe2016Egalitarian/terrestrialAcidificationPotential.py +2 -2
- hestia_earth/models/recipe2016Hierarchist/ecosystemDamageOzoneFormation.py +2 -2
- hestia_earth/models/recipe2016Hierarchist/freshwaterEutrophicationPotential.py +2 -2
- hestia_earth/models/recipe2016Hierarchist/humanDamageOzoneFormation.py +2 -2
- hestia_earth/models/recipe2016Hierarchist/marineEutrophicationPotential.py +2 -2
- hestia_earth/models/recipe2016Hierarchist/ozoneDepletionPotential.py +2 -2
- hestia_earth/models/recipe2016Hierarchist/terrestrialAcidificationPotential.py +2 -2
- hestia_earth/models/recipe2016Individualist/ecosystemDamageOzoneFormation.py +2 -2
- hestia_earth/models/recipe2016Individualist/freshwaterEutrophicationPotential.py +2 -2
- hestia_earth/models/recipe2016Individualist/humanDamageOzoneFormation.py +2 -2
- hestia_earth/models/recipe2016Individualist/marineEutrophicationPotential.py +2 -2
- hestia_earth/models/recipe2016Individualist/ozoneDepletionPotential.py +2 -2
- hestia_earth/models/recipe2016Individualist/terrestrialAcidificationPotential.py +2 -2
- hestia_earth/models/schmidt2007/utils.py +13 -4
- hestia_earth/models/utils/blank_node.py +73 -3
- hestia_earth/models/utils/constant.py +8 -1
- hestia_earth/models/utils/cycle.py +10 -13
- hestia_earth/models/utils/fuel.py +1 -1
- hestia_earth/models/utils/impact_assessment.py +49 -24
- hestia_earth/models/utils/lookup.py +36 -7
- hestia_earth/models/utils/pesticideAI.py +1 -1
- hestia_earth/models/utils/property.py +11 -4
- hestia_earth/models/utils/term.py +15 -8
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/METADATA +2 -2
- {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/RECORD +123 -114
- {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/WHEEL +1 -1
- tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +196 -0
- tests/models/cml2001Baseline/test_abioticResourceDepletionMineralsAndMetals.py +124 -0
- tests/models/cycle/completeness/test_freshForage.py +21 -0
- tests/models/edip2003/test_ozoneDepletionPotential.py +1 -13
- tests/models/environmentalFootprintV3/test_soilQualityIndexLandTransformation.py +1 -2
- tests/models/impact_assessment/test_emissions.py +1 -0
- tests/models/ipcc2019/test_aboveGroundBiomass.py +27 -63
- tests/models/ipcc2019/test_belowGroundBiomass.py +146 -0
- tests/models/ipcc2019/test_biomass_utils.py +115 -0
- tests/models/ipcc2019/{test_co2ToAirAboveGroundBiomassStockChangeLandUseChange.py → test_co2ToAirAboveGroundBiomassStockChange.py} +5 -5
- tests/models/ipcc2019/{test_co2ToAirBelowGroundBiomassStockChangeLandUseChange.py → test_co2ToAirBelowGroundBiomassStockChange.py} +5 -5
- tests/models/ipcc2019/{test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py → test_co2ToAirSoilOrganicCarbonStockChange.py} +5 -5
- tests/models/ipcc2021/test_gwp100.py +2 -2
- tests/models/utils/test_impact_assessment.py +3 -3
- hestia_earth/models/ipcc2019/aboveGroundBiomass_utils.py +0 -180
- tests/models/ipcc2019/test_aboveGroundBiomass_utils.py +0 -92
- {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from functools import reduce
|
|
3
|
+
from numpy import average, copy, random, vstack
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
from hestia_earth.schema import (
|
|
8
|
+
MeasurementMethodClassification,
|
|
9
|
+
MeasurementStatsDefinition,
|
|
10
|
+
SiteSiteType,
|
|
11
|
+
TermTermType
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from hestia_earth.utils.model import filter_list_term_type
|
|
15
|
+
from hestia_earth.utils.tools import non_empty_list
|
|
16
|
+
|
|
17
|
+
from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
|
|
18
|
+
from hestia_earth.models.utils import pairwise
|
|
19
|
+
from hestia_earth.models.utils.array_builders import gen_seed
|
|
20
|
+
from hestia_earth.models.utils.blank_node import group_nodes_by_year
|
|
21
|
+
from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
|
|
22
|
+
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
23
|
+
from hestia_earth.models.utils.measurement import _new_measurement
|
|
24
|
+
|
|
25
|
+
from . import MODEL
|
|
26
|
+
from .biomass_utils import (
|
|
27
|
+
BiomassCategory, detect_land_cover_change, group_by_biomass_category, sample_biomass_equilibrium,
|
|
28
|
+
summarise_land_cover_nodes
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
REQUIREMENTS = {
|
|
33
|
+
"Site": {
|
|
34
|
+
"management": [
|
|
35
|
+
{
|
|
36
|
+
"@type": "Management",
|
|
37
|
+
"value": "",
|
|
38
|
+
"term.termType": "landCover",
|
|
39
|
+
"endDate": "",
|
|
40
|
+
"optional": {
|
|
41
|
+
"startDate": ""
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"measurements": [
|
|
46
|
+
{
|
|
47
|
+
"@type": "Measurement",
|
|
48
|
+
"value": ["1", "2", "3", "4", "7", "8", "9", "10", "11", "12"],
|
|
49
|
+
"term.@id": "ecoClimateZone"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"none": {
|
|
53
|
+
"siteType": ["glass or high accessible cover"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
LOOKUPS = {
|
|
58
|
+
"landCover": "BIOMASS_CATEGORY",
|
|
59
|
+
"ecoClimateZone": [
|
|
60
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_FOREST",
|
|
61
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_NATURAL_FOREST",
|
|
62
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_PLANTATION_FOREST",
|
|
63
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_OTHER"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
RETURNS = {
|
|
67
|
+
"Measurement": [{
|
|
68
|
+
"value": "",
|
|
69
|
+
"sd": "",
|
|
70
|
+
"min": "",
|
|
71
|
+
"max": "",
|
|
72
|
+
"statsDefinition": "simulated",
|
|
73
|
+
"observations": "",
|
|
74
|
+
"dates": "",
|
|
75
|
+
"methodClassification": "tier 1 model"
|
|
76
|
+
}]
|
|
77
|
+
}
|
|
78
|
+
TERM_ID = 'belowGroundBiomass'
|
|
79
|
+
|
|
80
|
+
_ITERATIONS = 10000
|
|
81
|
+
_METHOD_CLASSIFICATION = MeasurementMethodClassification.TIER_1_MODEL.value
|
|
82
|
+
_STATS_DEFINITION = MeasurementStatsDefinition.SIMULATED.value
|
|
83
|
+
|
|
84
|
+
_LAND_COVER_TERM_TYPE = TermTermType.LANDCOVER
|
|
85
|
+
|
|
86
|
+
_EQUILIBRIUM_TRANSITION_PERIOD = 20
|
|
87
|
+
_EXCLUDED_ECO_CLIMATE_ZONES = {EcoClimateZone.POLAR_MOIST, EcoClimateZone.POLAR_DRY}
|
|
88
|
+
_EXCLUDED_SITE_TYPES = {
|
|
89
|
+
SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_VALID_BIOMASS_CATEGORIES = {
|
|
93
|
+
BiomassCategory.FOREST,
|
|
94
|
+
BiomassCategory.NATURAL_FOREST,
|
|
95
|
+
BiomassCategory.PLANTATION_FOREST
|
|
96
|
+
}
|
|
97
|
+
"""Biomass stock data is only available in the lookups for these categories."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _InventoryKey(Enum):
|
|
101
|
+
"""
|
|
102
|
+
The inner keys of the annualised inventory created by the `_compile_inventory` function.
|
|
103
|
+
|
|
104
|
+
The value of each enum member is formatted to be used as a column header in the `log_as_table` function.
|
|
105
|
+
"""
|
|
106
|
+
BIOMASS_CATEGORY_SUMMARY = "biomass-categories"
|
|
107
|
+
LAND_COVER_CHANGE_EVENT = "lcc-event"
|
|
108
|
+
YEARS_SINCE_LCC_EVENT = "years-since-lcc-event"
|
|
109
|
+
REGIME_START_YEAR = "regime-start-year"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_REQUIRED_INVENTORY_KEYS = [e for e in _InventoryKey]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run(site: dict) -> list[dict]:
|
|
116
|
+
"""
|
|
117
|
+
Run the model on a Site.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
site : dict
|
|
122
|
+
A valid HESTIA [Site](https://www.hestia.earth/schema/Site).
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
list[dict]
|
|
127
|
+
A list of HESTIA [Measurement](https://www.hestia.earth/schema/Measurement) nodes with `term.termType` =
|
|
128
|
+
`aboveGroundBiomass`
|
|
129
|
+
"""
|
|
130
|
+
should_run, inventory, kwargs = _should_run(site)
|
|
131
|
+
return _run(inventory, iterations=_ITERATIONS, **kwargs) if should_run else []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _should_run(site: dict) -> tuple[bool, dict, dict]:
|
|
135
|
+
"""
|
|
136
|
+
Extract and re-organise required data from the input [Site](https://www.hestia.earth/schema/Site) node and determine
|
|
137
|
+
whether the model should run.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
site : dict
|
|
142
|
+
A valid HESTIA [Site](https://www.hestia.earth/schema/Site).
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
tuple[bool, dict, dict]
|
|
147
|
+
should_run, inventory, kwargs
|
|
148
|
+
"""
|
|
149
|
+
site_type = site.get("siteType")
|
|
150
|
+
eco_climate_zone = get_eco_climate_zone_value(site, as_enum=True)
|
|
151
|
+
|
|
152
|
+
land_cover = filter_list_term_type(site.get("management", []), _LAND_COVER_TERM_TYPE)
|
|
153
|
+
|
|
154
|
+
has_valid_site_type = site_type not in _EXCLUDED_SITE_TYPES
|
|
155
|
+
has_valid_eco_climate_zone = all([
|
|
156
|
+
eco_climate_zone,
|
|
157
|
+
eco_climate_zone not in _EXCLUDED_ECO_CLIMATE_ZONES
|
|
158
|
+
])
|
|
159
|
+
has_land_cover_nodes = len(land_cover) > 0
|
|
160
|
+
|
|
161
|
+
should_compile_inventory = all([
|
|
162
|
+
has_valid_site_type,
|
|
163
|
+
has_valid_eco_climate_zone,
|
|
164
|
+
has_land_cover_nodes
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
inventory = _compile_inventory(land_cover) if should_compile_inventory else {}
|
|
168
|
+
kwargs = {
|
|
169
|
+
"eco_climate_zone": eco_climate_zone,
|
|
170
|
+
"seed": gen_seed(site)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logRequirements(
|
|
174
|
+
site, model=MODEL, term=TERM_ID,
|
|
175
|
+
site_type=site_type,
|
|
176
|
+
has_valid_site_type=has_valid_site_type,
|
|
177
|
+
has_valid_eco_climate_zone=has_valid_eco_climate_zone,
|
|
178
|
+
has_land_cover_nodes=has_land_cover_nodes,
|
|
179
|
+
**kwargs,
|
|
180
|
+
inventory=_format_inventory(inventory)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
should_run = all([
|
|
184
|
+
len(inventory) > 0,
|
|
185
|
+
all(data for data in inventory.values() if all(key in data.keys() for key in _REQUIRED_INVENTORY_KEYS))
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
logShouldRun(site, MODEL, TERM_ID, should_run)
|
|
189
|
+
|
|
190
|
+
return should_run, inventory, kwargs
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
|
|
194
|
+
"""
|
|
195
|
+
Build an annual inventory of model input data.
|
|
196
|
+
|
|
197
|
+
Returns a dict with shape:
|
|
198
|
+
```
|
|
199
|
+
{
|
|
200
|
+
year (int): {
|
|
201
|
+
_InventoryKey.BIOMASS_CATEGORY_SUMMARY: {
|
|
202
|
+
category (BiomassCategory): value (float),
|
|
203
|
+
...categories
|
|
204
|
+
},
|
|
205
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: value (bool),
|
|
206
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: value (int),
|
|
207
|
+
_InventoryKey.REGIME_START_YEAR: value (int)
|
|
208
|
+
},
|
|
209
|
+
...years
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
land_cover_nodes : list[dict]
|
|
216
|
+
A list of HESTIA [Management](https://www.hestia.earth/schema/Measurement) nodes with `term.termType` =
|
|
217
|
+
`landCover`
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
dict
|
|
222
|
+
The inventory of data.
|
|
223
|
+
"""
|
|
224
|
+
land_cover_grouped = group_nodes_by_year(land_cover_nodes)
|
|
225
|
+
|
|
226
|
+
def build_inventory_year(inventory: dict, year_pair: tuple[int, int]) -> dict:
|
|
227
|
+
"""
|
|
228
|
+
Build a year of the inventory using the data from `land_cover_categories_grouped`.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
inventory: dict
|
|
233
|
+
The land cover change portion of the inventory. Must have the same shape as the returned dict.
|
|
234
|
+
year_pair : tuple[int, int]
|
|
235
|
+
A tuple with the shape `(prev_year, current_year)`.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
dict
|
|
240
|
+
The land cover change portion of the inventory.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
prev_year, current_year = year_pair
|
|
244
|
+
land_cover_nodes = land_cover_grouped.get(current_year, {})
|
|
245
|
+
|
|
246
|
+
biomass_category_summary = summarise_land_cover_nodes(land_cover_nodes, group_by_biomass_category)
|
|
247
|
+
|
|
248
|
+
prev_biomass_category_summary = inventory.get(prev_year, {}).get(_InventoryKey.BIOMASS_CATEGORY_SUMMARY, {})
|
|
249
|
+
is_lcc_event = detect_land_cover_change(biomass_category_summary, prev_biomass_category_summary)
|
|
250
|
+
|
|
251
|
+
time_delta = current_year - prev_year
|
|
252
|
+
prev_years_since_lcc_event = inventory.get(prev_year, {}).get(_InventoryKey.YEARS_SINCE_LCC_EVENT, 0)
|
|
253
|
+
years_since_lcc_event = time_delta if is_lcc_event else prev_years_since_lcc_event + time_delta
|
|
254
|
+
regime_start_year = current_year - years_since_lcc_event
|
|
255
|
+
|
|
256
|
+
update_dict = {
|
|
257
|
+
current_year: {
|
|
258
|
+
_InventoryKey.BIOMASS_CATEGORY_SUMMARY: biomass_category_summary,
|
|
259
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: is_lcc_event,
|
|
260
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: years_since_lcc_event,
|
|
261
|
+
_InventoryKey.REGIME_START_YEAR: regime_start_year
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return inventory | update_dict
|
|
265
|
+
|
|
266
|
+
start_year = list(land_cover_grouped)[0]
|
|
267
|
+
initial_land_cover_nodes = land_cover_grouped.get(start_year, {})
|
|
268
|
+
|
|
269
|
+
initial = {
|
|
270
|
+
start_year: {
|
|
271
|
+
_InventoryKey.BIOMASS_CATEGORY_SUMMARY: summarise_land_cover_nodes(
|
|
272
|
+
initial_land_cover_nodes, group_by_biomass_category
|
|
273
|
+
),
|
|
274
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: False,
|
|
275
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: _EQUILIBRIUM_TRANSITION_PERIOD,
|
|
276
|
+
_InventoryKey.REGIME_START_YEAR: start_year - _EQUILIBRIUM_TRANSITION_PERIOD
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return reduce(
|
|
281
|
+
build_inventory_year,
|
|
282
|
+
pairwise(land_cover_grouped.keys()), # Inventory years need data from previous year to be compiled.
|
|
283
|
+
initial
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _format_inventory(inventory: dict) -> str:
|
|
288
|
+
"""
|
|
289
|
+
Format the SOC inventory for logging as a table. Rows represent inventory years, columns represent soc stock change
|
|
290
|
+
data for each measurement method classification present in inventory. If the inventory is invalid, return `"None"`
|
|
291
|
+
as a string.
|
|
292
|
+
"""
|
|
293
|
+
inventory_years = sorted(set(non_empty_list(years for years in inventory.keys())))
|
|
294
|
+
land_covers = _get_unique_categories(inventory, _InventoryKey.BIOMASS_CATEGORY_SUMMARY)
|
|
295
|
+
inventory_keys = _get_loggable_inventory_keys(inventory)
|
|
296
|
+
|
|
297
|
+
should_run = inventory and len(inventory_years) > 0
|
|
298
|
+
|
|
299
|
+
return log_as_table(
|
|
300
|
+
{
|
|
301
|
+
"year": year,
|
|
302
|
+
**{
|
|
303
|
+
_format_column_header(category): _format_number(
|
|
304
|
+
inventory.get(year, {}).get(_InventoryKey.BIOMASS_CATEGORY_SUMMARY, {}).get(category, 0)
|
|
305
|
+
) for category in land_covers
|
|
306
|
+
},
|
|
307
|
+
**{
|
|
308
|
+
_format_column_header(key): _INVENTORY_KEY_TO_FORMAT_FUNC[key](
|
|
309
|
+
inventory.get(year, {}).get(key)
|
|
310
|
+
) for key in inventory_keys
|
|
311
|
+
}
|
|
312
|
+
} for year in inventory_years
|
|
313
|
+
) if should_run else "None"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _get_unique_categories(inventory: dict, key: _InventoryKey) -> list:
|
|
317
|
+
"""
|
|
318
|
+
Extract the unique biomass or land cover categories from the inventory.
|
|
319
|
+
|
|
320
|
+
Can be used to cache sampled parameters for each `BiomassCategory` or to log land covers.
|
|
321
|
+
"""
|
|
322
|
+
categories = reduce(
|
|
323
|
+
lambda result, categories: result | set(categories),
|
|
324
|
+
(inner.get(key, {}).keys() for inner in inventory.values()),
|
|
325
|
+
set()
|
|
326
|
+
)
|
|
327
|
+
return sorted(
|
|
328
|
+
categories,
|
|
329
|
+
key=lambda category: category.value if isinstance(category, Enum) else str(category),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _get_loggable_inventory_keys(inventory: dict) -> list:
|
|
334
|
+
"""
|
|
335
|
+
Return a list of unique inventory keys in a fixed order.
|
|
336
|
+
"""
|
|
337
|
+
unique_keys = reduce(
|
|
338
|
+
lambda result, keys: result | set(keys),
|
|
339
|
+
(
|
|
340
|
+
(key for key in group.keys() if key in _INVENTORY_KEY_TO_FORMAT_FUNC)
|
|
341
|
+
for group in inventory.values()
|
|
342
|
+
),
|
|
343
|
+
set()
|
|
344
|
+
)
|
|
345
|
+
key_order = {key: i for i, key in enumerate(_INVENTORY_KEY_TO_FORMAT_FUNC.keys())}
|
|
346
|
+
return sorted(unique_keys, key=lambda key_: key_order[key_])
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _format_bool(value: Optional[bool]) -> str:
|
|
350
|
+
"""Format a bool for logging in a table."""
|
|
351
|
+
return str(bool(value))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _format_number(value: Optional[float]) -> str:
|
|
355
|
+
"""Format a float for logging in a table."""
|
|
356
|
+
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _format_column_header(value: Union[_InventoryKey, BiomassCategory, str]):
|
|
360
|
+
"""Format an enum or str for logging as a table column header."""
|
|
361
|
+
as_string = value.value if isinstance(value, Enum) else str(value)
|
|
362
|
+
return as_string.replace(" ", "-")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
_INVENTORY_KEY_TO_FORMAT_FUNC = {
|
|
366
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: _format_bool,
|
|
367
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: _format_number
|
|
368
|
+
}
|
|
369
|
+
"""
|
|
370
|
+
Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
|
|
371
|
+
the `dict` keys.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _run(
|
|
376
|
+
inventory: dict,
|
|
377
|
+
*,
|
|
378
|
+
eco_climate_zone: EcoClimateZone,
|
|
379
|
+
iterations: int,
|
|
380
|
+
seed: Union[int, random.Generator, None] = None
|
|
381
|
+
) -> list[dict]:
|
|
382
|
+
"""
|
|
383
|
+
Calculate the annual above ground biomass stock based on an inventory of land cover data.
|
|
384
|
+
|
|
385
|
+
Inventory should be a dict with shape:
|
|
386
|
+
```
|
|
387
|
+
{
|
|
388
|
+
year (int): {
|
|
389
|
+
_InventoryKey.BIOMASS_CATEGORY_SUMMARY: {
|
|
390
|
+
category (BiomassCategory): value (float),
|
|
391
|
+
...categories
|
|
392
|
+
},
|
|
393
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: value (bool),
|
|
394
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: value (int),
|
|
395
|
+
_InventoryKey.REGIME_START_YEAR: value (int)
|
|
396
|
+
},
|
|
397
|
+
...years
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
inventory : dict
|
|
404
|
+
The annual inventory of land cover data.
|
|
405
|
+
ecoClimateZone : EcoClimateZone
|
|
406
|
+
The eco-climate zone of the site.
|
|
407
|
+
iterations: int
|
|
408
|
+
The number of iterations to run the model as a Monte Carlo simulation.
|
|
409
|
+
seed : int | random.Generator | None
|
|
410
|
+
The seed for the random sampling of model parameters.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
list[dict]
|
|
415
|
+
A list of HESTIA [Measurement](https://www.hestia.earth/schema/Measurement) nodes with `term.termType` =
|
|
416
|
+
`aboveGroundBiomass`
|
|
417
|
+
"""
|
|
418
|
+
rng = random.default_rng(seed)
|
|
419
|
+
unique_biomass_categories = _get_unique_categories(inventory, _InventoryKey.BIOMASS_CATEGORY_SUMMARY)
|
|
420
|
+
|
|
421
|
+
timestamps = list(inventory.keys())
|
|
422
|
+
|
|
423
|
+
factor_cache = {
|
|
424
|
+
category: sample_biomass_equilibrium(iterations, category, eco_climate_zone, _build_col_name, seed=rng)
|
|
425
|
+
for category in unique_biomass_categories
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
def get_average_equilibrium(year) -> NDArray:
|
|
429
|
+
biomass_categories = inventory.get(year, {}).get(_InventoryKey.BIOMASS_CATEGORY_SUMMARY, {})
|
|
430
|
+
values = [factor_cache.get(category) for category in biomass_categories.keys()]
|
|
431
|
+
weights = [weight for weight in biomass_categories.values()]
|
|
432
|
+
return average(values, axis=0, weights=weights)
|
|
433
|
+
|
|
434
|
+
equilibrium_annual = vstack([get_average_equilibrium(year) for year in inventory.keys()])
|
|
435
|
+
|
|
436
|
+
def calc_biomass_stock(result: NDArray, index_year: tuple[int, int]) -> NDArray:
|
|
437
|
+
index, year = index_year
|
|
438
|
+
|
|
439
|
+
years_since_llc_event = inventory.get(year, {}).get(_InventoryKey.YEARS_SINCE_LCC_EVENT, 0)
|
|
440
|
+
regime_start_year = inventory.get(year, {}).get(_InventoryKey.REGIME_START_YEAR, 0)
|
|
441
|
+
regime_start_index = (
|
|
442
|
+
timestamps.index(regime_start_year) if regime_start_year in timestamps else 0
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
regime_start_biomass = result[regime_start_index]
|
|
446
|
+
current_biomass_equilibrium = equilibrium_annual[index]
|
|
447
|
+
|
|
448
|
+
time_ratio = min(years_since_llc_event / _EQUILIBRIUM_TRANSITION_PERIOD, 1)
|
|
449
|
+
biomass_delta = (current_biomass_equilibrium - regime_start_biomass) * time_ratio
|
|
450
|
+
|
|
451
|
+
result[index] = regime_start_biomass + biomass_delta
|
|
452
|
+
return result
|
|
453
|
+
|
|
454
|
+
biomass_annual = reduce(
|
|
455
|
+
calc_biomass_stock,
|
|
456
|
+
list(enumerate(timestamps))[1:],
|
|
457
|
+
copy(equilibrium_annual)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
descriptive_stats = calc_descriptive_stats(
|
|
461
|
+
biomass_annual,
|
|
462
|
+
_STATS_DEFINITION,
|
|
463
|
+
axis=1, # Calculate stats rowwise.
|
|
464
|
+
decimals=6 # Round values to the nearest milligram.
|
|
465
|
+
)
|
|
466
|
+
return [_measurement(timestamps, **descriptive_stats)]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _build_col_name(biomass_category: BiomassCategory) -> str:
|
|
470
|
+
"""
|
|
471
|
+
Get the column name for the `ecoClimateZone-lookup.csv` for a specific biomass category equilibrium.
|
|
472
|
+
"""
|
|
473
|
+
COL_NAME_ROOT = "BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_"
|
|
474
|
+
return (
|
|
475
|
+
f"{COL_NAME_ROOT}{biomass_category.name}" if biomass_category in _VALID_BIOMASS_CATEGORIES
|
|
476
|
+
else f"{COL_NAME_ROOT}OTHER"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _measurement(
|
|
481
|
+
timestamps: list[int],
|
|
482
|
+
value: list[float],
|
|
483
|
+
*,
|
|
484
|
+
sd: list[float] = None,
|
|
485
|
+
min: list[float] = None,
|
|
486
|
+
max: list[float] = None,
|
|
487
|
+
statsDefinition: str = None,
|
|
488
|
+
observations: list[int] = None
|
|
489
|
+
) -> dict:
|
|
490
|
+
"""
|
|
491
|
+
Build a Hestia `Measurement` node to contain a value and descriptive statistics calculated by the models.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
timestamps : list[int]
|
|
496
|
+
A list of calendar years associated to the calculated SOC stocks.
|
|
497
|
+
value : list[float]
|
|
498
|
+
A list of values representing the mean biomass stock for each year of the inventory
|
|
499
|
+
sd : list[float]
|
|
500
|
+
A list of standard deviations representing the standard deviation of the biomass stock for each year of the
|
|
501
|
+
inventory.
|
|
502
|
+
min : list[float]
|
|
503
|
+
A list of minimum values representing the minimum modelled biomass stock for each year of the inventory.
|
|
504
|
+
max : list[float]
|
|
505
|
+
A list of maximum values representing the maximum modelled biomass stock for each year of the inventory.
|
|
506
|
+
statsDefinition : str
|
|
507
|
+
The [statsDefinition](https://www-staging.hestia.earth/schema/Measurement#statsDefinition) of the measurement.
|
|
508
|
+
observations : list[int]
|
|
509
|
+
The number of model iterations used to calculate the descriptive statistics.
|
|
510
|
+
|
|
511
|
+
Returns
|
|
512
|
+
-------
|
|
513
|
+
dict
|
|
514
|
+
A valid HESTIA `Measurement` node, see: https://www.hestia.earth/schema/Measurement.
|
|
515
|
+
"""
|
|
516
|
+
update_dict = {
|
|
517
|
+
"value": value,
|
|
518
|
+
"sd": sd,
|
|
519
|
+
"min": min,
|
|
520
|
+
"max": max,
|
|
521
|
+
"statsDefinition": statsDefinition,
|
|
522
|
+
"observations": observations,
|
|
523
|
+
"dates": [f"{year}-12-31" for year in timestamps],
|
|
524
|
+
"methodClassification": _METHOD_CLASSIFICATION
|
|
525
|
+
}
|
|
526
|
+
measurement = _new_measurement(TERM_ID) | {
|
|
527
|
+
key: value for key, value in update_dict.items() if value
|
|
528
|
+
}
|
|
529
|
+
return measurement
|