hestia-earth-models 0.64.8__py3-none-any.whl → 0.64.10__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/cycle/siteArea.py +2 -1
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandOccupation.py +73 -82
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandTransformation.py +102 -116
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexTotalLandUseEffects.py +27 -16
- hestia_earth/models/faostat2018/landTransformationFromCropland100YearAverage.py +3 -2
- hestia_earth/models/faostat2018/landTransformationFromCropland20YearAverage.py +3 -2
- hestia_earth/models/frischknechtEtAl2000/ionisingRadiationKbqU235Eq.py +69 -37
- hestia_earth/models/ipcc2019/aboveGroundBiomass.py +31 -243
- hestia_earth/models/ipcc2019/animal/fatContent.py +38 -0
- hestia_earth/models/ipcc2019/animal/liveweightGain.py +3 -54
- hestia_earth/models/ipcc2019/animal/liveweightPerHead.py +3 -54
- hestia_earth/models/ipcc2019/animal/pregnancyRateTotal.py +38 -0
- hestia_earth/models/ipcc2019/animal/trueProteinContent.py +38 -0
- hestia_earth/models/ipcc2019/animal/utils.py +87 -3
- hestia_earth/models/ipcc2019/animal/weightAtMaturity.py +4 -10
- 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/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.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/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsMarineEutrophication.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/damageToHumanHealthParticulateMatterFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
- hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.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 +3078 -575
- hestia_earth/models/poschEtAl2008/terrestrialAcidificationPotentialAccumulatedExceedance.py +6 -3
- hestia_earth/models/poschEtAl2008/terrestrialEutrophicationPotentialAccumulatedExceedance.py +6 -3
- hestia_earth/models/preload_requests.py +1 -1
- hestia_earth/models/schmidt2007/utils.py +13 -4
- hestia_earth/models/utils/__init__.py +5 -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 +39 -15
- 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.8.dist-info → hestia_earth_models-0.64.10.dist-info}/METADATA +2 -2
- {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/RECORD +103 -90
- {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/WHEEL +1 -1
- tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +196 -0
- tests/models/cml2001Baseline/test_abioticResourceDepletionMineralsAndMetals.py +124 -0
- tests/models/edip2003/test_ozoneDepletionPotential.py +1 -13
- tests/models/environmentalFootprintV3/test_soilQualityIndexLandOccupation.py +97 -66
- tests/models/environmentalFootprintV3/test_soilQualityIndexLandTransformation.py +136 -74
- tests/models/environmentalFootprintV3/test_soilQualityIndexTotalLandUseEffects.py +15 -10
- tests/models/frischknechtEtAl2000/test_ionisingRadiationKbqU235Eq.py +67 -44
- tests/models/impact_assessment/test_emissions.py +1 -0
- tests/models/ipcc2019/animal/test_fatContent.py +22 -0
- tests/models/ipcc2019/animal/test_liveweightGain.py +4 -2
- tests/models/ipcc2019/animal/test_liveweightPerHead.py +4 -2
- tests/models/ipcc2019/animal/test_pregnancyRateTotal.py +22 -0
- tests/models/ipcc2019/animal/test_trueProteinContent.py +22 -0
- tests/models/ipcc2019/animal/test_weightAtMaturity.py +2 -1
- 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/poschEtAl2008/test_terrestrialAcidificationPotentialAccumulatedExceedance.py +30 -17
- tests/models/poschEtAl2008/test_terrestrialEutrophicationPotentialAccumulatedExceedance.py +28 -14
- hestia_earth/models/ipcc2019/aboveGroundBiomass_utils.py +0 -180
- tests/models/ipcc2019/test_aboveGroundBiomass_utils.py +0 -92
- {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from functools import reduce
|
|
3
|
+
from math import isclose
|
|
4
|
+
from numpy import random
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
from typing import Callable, Optional, Union
|
|
7
|
+
|
|
8
|
+
from hestia_earth.utils.blank_node import get_node_value
|
|
9
|
+
|
|
10
|
+
from hestia_earth.models.utils.array_builders import repeat_single, truncated_normal_1d
|
|
11
|
+
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_ecoClimateZone_lookup_grouped_value
|
|
12
|
+
from hestia_earth.models.utils.term import get_lookup_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOOKUPS = {
|
|
16
|
+
"landCover": "BIOMASS_CATEGORY",
|
|
17
|
+
"ecoClimateZone": [
|
|
18
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_ANNUAL_CROPS",
|
|
19
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_COCONUT",
|
|
20
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_FOREST",
|
|
21
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_GRASSLAND",
|
|
22
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_JATROPHA",
|
|
23
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_JOJOBA",
|
|
24
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_NATURAL_FOREST",
|
|
25
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_OIL_PALM",
|
|
26
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_OLIVE",
|
|
27
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_ORCHARD",
|
|
28
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_PLANTATION_FOREST",
|
|
29
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_RUBBER",
|
|
30
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_SHORT_ROTATION_COPPICE",
|
|
31
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_TEA",
|
|
32
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_VINE",
|
|
33
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_WOODY_PERENNIAL",
|
|
34
|
+
"AG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_OTHER",
|
|
35
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_NATURAL_FOREST",
|
|
36
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_PLANTATION_FOREST",
|
|
37
|
+
"BG_BIOMASS_EQUILIBRIUM_KG_C_HECTARE_OTHER"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BiomassCategory(Enum):
|
|
43
|
+
"""
|
|
44
|
+
Enum representing biomass categories, sourced from IPCC (2006), IPCC (2019) and European Commission (2010).
|
|
45
|
+
|
|
46
|
+
Enum values formatted for logging as table.
|
|
47
|
+
"""
|
|
48
|
+
ANNUAL_CROPS = "annual-crops"
|
|
49
|
+
COCONUT = "coconut" # European Commission (2010)
|
|
50
|
+
FOREST = "forest" # IPCC (2019) recalculated per eco-climate zone
|
|
51
|
+
GRASSLAND = "grassland"
|
|
52
|
+
JATROPHA = "jatropha" # European Commission (2010)
|
|
53
|
+
JOJOBA = "jojoba" # European Commission (2010)
|
|
54
|
+
NATURAL_FOREST = "natural-forest" # IPCC (2019) recalculated per eco-climate zone
|
|
55
|
+
OIL_PALM = "oil palm" # IPCC (2019)
|
|
56
|
+
OLIVE = "olive" # IPCC (2019)
|
|
57
|
+
ORCHARD = "orchard" # IPCC (2019)
|
|
58
|
+
OTHER = "other"
|
|
59
|
+
PLANTATION_FOREST = "plantation-forest" # IPCC (2019) recalculated per eco-climate zone
|
|
60
|
+
RUBBER = "rubber" # IPCC (2019)
|
|
61
|
+
SHORT_ROTATION_COPPICE = "short-rotation-coppice" # IPCC (2019)
|
|
62
|
+
TEA = "tea" # IPCC (2019)
|
|
63
|
+
VINE = "vine" # IPCC (2019)
|
|
64
|
+
WOODY_PERENNIAL = "woody-perennial" # IPCC (2006)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_BIOMASS_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE = {
|
|
68
|
+
BiomassCategory.ANNUAL_CROPS: "Annual crops",
|
|
69
|
+
BiomassCategory.COCONUT: "Coconut",
|
|
70
|
+
BiomassCategory.FOREST: "Forest",
|
|
71
|
+
BiomassCategory.GRASSLAND: "Grassland",
|
|
72
|
+
BiomassCategory.JATROPHA: "Jatropha",
|
|
73
|
+
BiomassCategory.JOJOBA: "Jojoba",
|
|
74
|
+
BiomassCategory.NATURAL_FOREST: "Natural forest",
|
|
75
|
+
BiomassCategory.OIL_PALM: "Oil palm",
|
|
76
|
+
BiomassCategory.OLIVE: "Olive",
|
|
77
|
+
BiomassCategory.ORCHARD: "Orchard",
|
|
78
|
+
BiomassCategory.OTHER: "Other",
|
|
79
|
+
BiomassCategory.PLANTATION_FOREST: "Plantation forest",
|
|
80
|
+
BiomassCategory.RUBBER: "Rubber",
|
|
81
|
+
BiomassCategory.SHORT_ROTATION_COPPICE: "Short rotation coppice",
|
|
82
|
+
BiomassCategory.TEA: "Tea",
|
|
83
|
+
BiomassCategory.VINE: "Vine",
|
|
84
|
+
BiomassCategory.WOODY_PERENNIAL: "Woody perennial"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_TARGET_LAND_COVER = 100
|
|
88
|
+
|
|
89
|
+
_GROUP_LAND_COVER_BY_BIOMASS_CATEGORY = [
|
|
90
|
+
BiomassCategory.ANNUAL_CROPS,
|
|
91
|
+
BiomassCategory.GRASSLAND,
|
|
92
|
+
BiomassCategory.OTHER,
|
|
93
|
+
BiomassCategory.SHORT_ROTATION_COPPICE
|
|
94
|
+
]
|
|
95
|
+
"""
|
|
96
|
+
Terms associated with these biomass categories can be grouped together when summarising land cover coverage in
|
|
97
|
+
`_group_by_term_id`.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def group_by_biomass_category(result: dict[BiomassCategory, float], node: dict) -> dict[BiomassCategory, float]:
|
|
102
|
+
"""
|
|
103
|
+
Reducer function for `_group_land_cover_nodes_by` that groups and sums node value by their associated
|
|
104
|
+
`BiomassCategory`.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
result : dict
|
|
109
|
+
A dict with the shape `{category (BiomassCategory): sum_value (float), ...categories}`.
|
|
110
|
+
node : dict
|
|
111
|
+
A HESTIA `Management` node with `term.termType` = `landCover`.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
result : dict
|
|
116
|
+
A dict with the shape `{category (BiomassCategory): sum_value (float), ...categories}`.
|
|
117
|
+
"""
|
|
118
|
+
biomass_category = _retrieve_biomass_category(node)
|
|
119
|
+
value = get_node_value(node)
|
|
120
|
+
|
|
121
|
+
update_dict = {biomass_category: result.get(biomass_category, 0) + value}
|
|
122
|
+
|
|
123
|
+
should_run = biomass_category and value
|
|
124
|
+
return result | update_dict if should_run else result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def group_by_term_id(
|
|
128
|
+
result: dict[Union[str, BiomassCategory], float], node: dict
|
|
129
|
+
) -> dict[Union[str, BiomassCategory], float]:
|
|
130
|
+
"""
|
|
131
|
+
Reducer function for `_group_land_cover_nodes_by` that groups and sums node value by their `term.@id` if a the land
|
|
132
|
+
cover is a woody plant, else by their associated `BiomassCategory`
|
|
133
|
+
|
|
134
|
+
Land cover events can be triggered by changes in land cover within the same `BiomassCategory` (e.g., `peachTree` to
|
|
135
|
+
`appleTree`) due to the requirement to clear the previous woody biomass to establish the new land cover.
|
|
136
|
+
|
|
137
|
+
Some land covers (e.g., land covers associated with the `BiomassCategory` = `Annual crops`, `Grassland`, `Other` or
|
|
138
|
+
`Short rotation coppice`) are exempt from this rule due to the Tier 1 assumptions that biomass does not accumulate
|
|
139
|
+
within the category or the maturity cycle of the land cover is significantly shorter than the amortisation period of
|
|
140
|
+
20 years.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
result : dict
|
|
145
|
+
A dict with the shape `{category (str | BiomassCategory): sum_value (float), ...categories}`.
|
|
146
|
+
node : dict
|
|
147
|
+
A HESTIA `Management` node with `term.termType` = `landCover`.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
result : dict
|
|
152
|
+
A dict with the shape `{category (str | BiomassCategory): sum_value (float), ...categories}`.
|
|
153
|
+
"""
|
|
154
|
+
term_id = node.get("term", {}).get("@id")
|
|
155
|
+
biomass_category = _retrieve_biomass_category(node)
|
|
156
|
+
value = get_node_value(node)
|
|
157
|
+
|
|
158
|
+
key = biomass_category if biomass_category in _GROUP_LAND_COVER_BY_BIOMASS_CATEGORY else term_id
|
|
159
|
+
|
|
160
|
+
update_dict = {key: result.get(key, 0) + value}
|
|
161
|
+
|
|
162
|
+
should_run = biomass_category and value
|
|
163
|
+
return result | update_dict if should_run else result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _retrieve_biomass_category(node: dict) -> Optional[BiomassCategory]:
|
|
167
|
+
"""
|
|
168
|
+
Retrieve the `BiomassCategory` associated with a land cover using the `BIOMASS_CATEGORY` lookup.
|
|
169
|
+
|
|
170
|
+
If lookup value is missing, return `None`.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
node : dict
|
|
175
|
+
A valid `Management` node with `term.termType` = `landCover`.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
BiomassCategory | None
|
|
180
|
+
The associated `BiomassCategory` or `None`
|
|
181
|
+
"""
|
|
182
|
+
LOOKUP = LOOKUPS["landCover"]
|
|
183
|
+
term = node.get("term", {})
|
|
184
|
+
lookup_value = get_lookup_value(term, LOOKUP, skip_debug=True)
|
|
185
|
+
|
|
186
|
+
return _assign_biomass_category(lookup_value) if lookup_value else None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def summarise_land_cover_nodes(
|
|
190
|
+
land_cover_nodes: list[dict],
|
|
191
|
+
group_by_func: Callable[[dict, dict], dict] = group_by_biomass_category
|
|
192
|
+
) -> dict[Union[str, BiomassCategory], float]:
|
|
193
|
+
"""
|
|
194
|
+
Group land cover nodes using `group_by_func`.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
land_cover_nodes : list[dict]
|
|
199
|
+
A list of HESTIA `Management` nodes with `term.termType` = `landCover`.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
result : dict
|
|
204
|
+
A dict with the shape `{category (str | BiomassCategory): sum_value (float), ...categories}`.
|
|
205
|
+
"""
|
|
206
|
+
category_cover = reduce(group_by_func, land_cover_nodes, dict())
|
|
207
|
+
return _rescale_category_cover(category_cover)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _rescale_category_cover(
|
|
211
|
+
category_cover: dict[Union[BiomassCategory, str], float]
|
|
212
|
+
) -> dict[Union[BiomassCategory, str], float]:
|
|
213
|
+
"""
|
|
214
|
+
Enforce a land cover coverage of 100%.
|
|
215
|
+
|
|
216
|
+
If input coverage is less than 100%, fill the remainder with `BiomassCategory.OTHER`. If the input coverage is
|
|
217
|
+
greater than 100%, proportionally downscale all categories.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
category_cover : dict[BiomassCategory | str, float]
|
|
222
|
+
The input category cover dict.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
result : dict[BiomassCategory | str, float]
|
|
227
|
+
The rescaled category cover dict.
|
|
228
|
+
"""
|
|
229
|
+
total_cover = sum(category_cover.values())
|
|
230
|
+
return (
|
|
231
|
+
_fill_category_cover(category_cover) if total_cover < _TARGET_LAND_COVER
|
|
232
|
+
else _squash_category_cover(category_cover) if total_cover > _TARGET_LAND_COVER
|
|
233
|
+
else category_cover
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _fill_category_cover(
|
|
238
|
+
category_cover: dict[Union[BiomassCategory, str], float]
|
|
239
|
+
) -> dict[Union[BiomassCategory, str], float]:
|
|
240
|
+
"""
|
|
241
|
+
Fill the land cover coverage with `BiomassCategory.OTHER` to enforce a total coverage of 100%.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
category_cover : dict[BiomassCategory | str, float]
|
|
246
|
+
The input category cover dict.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
result : dict[BiomassCategory | str, float]
|
|
251
|
+
The rescaled category cover dict.
|
|
252
|
+
"""
|
|
253
|
+
total_cover = sum(category_cover.values())
|
|
254
|
+
update_dict = {
|
|
255
|
+
BiomassCategory.OTHER: category_cover.get(BiomassCategory.OTHER, 0) + (_TARGET_LAND_COVER - total_cover)
|
|
256
|
+
}
|
|
257
|
+
return category_cover | update_dict
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _squash_category_cover(
|
|
261
|
+
category_cover: dict[Union[BiomassCategory, str], float]
|
|
262
|
+
) -> dict[Union[BiomassCategory, str], float]:
|
|
263
|
+
"""
|
|
264
|
+
Proportionally shrink all land cover categories to enforce a total coverage of 100%.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
category_cover : dict[BiomassCategory | str, float]
|
|
269
|
+
The input category cover dict.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
result : dict[BiomassCategory | str, float]
|
|
274
|
+
The rescaled category cover dict.
|
|
275
|
+
"""
|
|
276
|
+
total_cover = sum(category_cover.values())
|
|
277
|
+
return {
|
|
278
|
+
category: (cover / total_cover) * _TARGET_LAND_COVER
|
|
279
|
+
for category, cover in category_cover.items()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def detect_land_cover_change(
|
|
284
|
+
a: dict[Union[BiomassCategory, str], float],
|
|
285
|
+
b: dict[Union[BiomassCategory, str], float]
|
|
286
|
+
) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Land cover values (% area) are compared with an absolute tolerance of 0.0001, which is equivalent to 1 m2 per
|
|
289
|
+
hectare.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
a : dict[BiomassCategory | str, float]
|
|
294
|
+
The first land-cover summary dict.
|
|
295
|
+
b : dict[BiomassCategory | str, float]
|
|
296
|
+
The second land-cover summary dict.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
bool
|
|
301
|
+
Whether a land-cover change event has occured.
|
|
302
|
+
"""
|
|
303
|
+
keys_match = sorted(str(key) for key in b.keys()) == sorted(str(key) for key in a.keys())
|
|
304
|
+
values_close = all(
|
|
305
|
+
isclose(b.get(key), a.get(key, -999), abs_tol=0.0001) for key in b.keys()
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return not all([keys_match, values_close])
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _assign_biomass_category(lookup_value: str) -> BiomassCategory:
|
|
312
|
+
"""
|
|
313
|
+
Return the `BiomassCategory` enum member associated with the input lookup value. If lookup value is missing or
|
|
314
|
+
doesn't map to any category, return `None`.
|
|
315
|
+
"""
|
|
316
|
+
return next(
|
|
317
|
+
(key for key, value in _BIOMASS_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.items() if value == lookup_value),
|
|
318
|
+
None
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def sample_biomass_equilibrium(
|
|
323
|
+
iterations: int,
|
|
324
|
+
biomass_category: BiomassCategory,
|
|
325
|
+
eco_climate_zone: EcoClimateZone,
|
|
326
|
+
build_col_name_func: Callable[[BiomassCategory], str],
|
|
327
|
+
seed: Union[int, random.Generator, None] = None
|
|
328
|
+
) -> dict:
|
|
329
|
+
"""
|
|
330
|
+
Sample a biomass equilibrium using the function specified in `KWARGS_TO_SAMPLE_FUNC`.
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
iterations : int
|
|
335
|
+
The number of samples to take.
|
|
336
|
+
biomass_category : BiomassCategory
|
|
337
|
+
The biomass category of the land cover.
|
|
338
|
+
eco_climate_zone : EcoClimateZone
|
|
339
|
+
The eco-climate zone of the site.
|
|
340
|
+
build_col_name_func : Callable[[BiomassCategory], str]
|
|
341
|
+
Function to build the name of the lookup column for a biomass category stock.
|
|
342
|
+
seed : int | Generator | None, optional
|
|
343
|
+
A seed to initialize the BitGenerator. If passed a Generator, it will be returned unaltered. If `None`, then
|
|
344
|
+
fresh, unpredictable entropy will be pulled from the OS.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
NDArray
|
|
349
|
+
The sampled parameter as a numpy array with shape `(1, iterations)`.
|
|
350
|
+
"""
|
|
351
|
+
DEFAULT_LOOKUP_DATA = {"value": 0}
|
|
352
|
+
col_name = build_col_name_func(biomass_category)
|
|
353
|
+
kwargs = get_ecoClimateZone_lookup_grouped_value(eco_climate_zone.value, col_name, default=DEFAULT_LOOKUP_DATA)
|
|
354
|
+
func = _get_sample_func(kwargs)
|
|
355
|
+
return func(iterations=iterations, seed=seed, **kwargs)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _get_sample_func(kwargs: dict) -> Callable:
|
|
359
|
+
"""
|
|
360
|
+
Select the correct sample function for a parameter based on the distribution data available. All possible
|
|
361
|
+
parameters for the model should have, at a minimum, a `value`, meaning that no default function needs to be
|
|
362
|
+
specified.
|
|
363
|
+
|
|
364
|
+
This function has been extracted into it's own method to allow for mocking of sample function.
|
|
365
|
+
|
|
366
|
+
Keyword Args
|
|
367
|
+
------------
|
|
368
|
+
value : float
|
|
369
|
+
The distribution mean.
|
|
370
|
+
sd : float
|
|
371
|
+
The standard deviation of the distribution.
|
|
372
|
+
uncertainty : float
|
|
373
|
+
The +/- uncertainty of the 95% confidence interval expressed as a percentage of the mean.
|
|
374
|
+
error : float
|
|
375
|
+
Two standard deviations expressed as a percentage of the mean.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
Callable
|
|
380
|
+
The sample function for the distribution.
|
|
381
|
+
"""
|
|
382
|
+
return next(
|
|
383
|
+
sample_func for required_kwargs, sample_func in _KWARGS_TO_SAMPLE_FUNC.items()
|
|
384
|
+
if all(kwarg in kwargs.keys() for kwarg in required_kwargs)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def sample_plus_minus_error(
|
|
389
|
+
*, iterations: int, value: float, error: float, seed: Optional[int] = None, **_
|
|
390
|
+
) -> NDArray:
|
|
391
|
+
"""Randomly sample a model parameter with a truncated normal distribution described using plus/minus error."""
|
|
392
|
+
sd = value * (error / 200)
|
|
393
|
+
low = value - (value * (error / 100))
|
|
394
|
+
high = value + (value * (error / 100))
|
|
395
|
+
return truncated_normal_1d(shape=(1, iterations), mu=value, sigma=sd, low=low, high=high, seed=seed)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def sample_constant(*, iterations: int, value: float, **_) -> NDArray:
|
|
399
|
+
"""Sample a constant model parameter."""
|
|
400
|
+
return repeat_single(shape=(1, iterations), value=value)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
_KWARGS_TO_SAMPLE_FUNC = {
|
|
404
|
+
("value", "error"): sample_plus_minus_error,
|
|
405
|
+
("value",): sample_constant
|
|
406
|
+
}
|
|
@@ -4,6 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
|
|
|
4
4
|
from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match
|
|
5
5
|
from hestia_earth.models.utils.emission import _new_emission
|
|
6
6
|
|
|
7
|
+
from .biomass_utils import detect_land_cover_change, summarise_land_cover_nodes
|
|
7
8
|
from .co2ToAirCarbonStockChange_utils import create_run_function, create_should_run_function
|
|
8
9
|
from . import MODEL
|
|
9
10
|
|
|
@@ -39,7 +40,10 @@ RETURNS = {
|
|
|
39
40
|
"methodTier": ""
|
|
40
41
|
}]
|
|
41
42
|
}
|
|
42
|
-
TERM_ID = 'co2ToAirAboveGroundBiomassStockChangeLandUseChange'
|
|
43
|
+
TERM_ID = 'co2ToAirAboveGroundBiomassStockChangeLandUseChange,co2ToAirAboveGroundBiomassStockChangeManagementChange'
|
|
44
|
+
|
|
45
|
+
_LU_EMISSION_TERM_ID = "co2ToAirAboveGroundBiomassStockChangeLandUseChange"
|
|
46
|
+
_MG_EMISSION_TERM_ID = "co2ToAirAboveGroundBiomassStockChangeManagementChange"
|
|
43
47
|
|
|
44
48
|
_CARBON_STOCK_TERM_ID = 'aboveGroundBiomass'
|
|
45
49
|
|
|
@@ -66,6 +70,7 @@ _SITE_TYPE_SYSTEMS_MAPPING = {
|
|
|
66
70
|
|
|
67
71
|
def _emission(
|
|
68
72
|
*,
|
|
73
|
+
term_id: str,
|
|
69
74
|
value: list[float],
|
|
70
75
|
method_tier: EmissionMethodTier,
|
|
71
76
|
sd: list[float] = None,
|
|
@@ -102,7 +107,7 @@ def _emission(
|
|
|
102
107
|
"observations": observations,
|
|
103
108
|
"methodTier": method_tier.value
|
|
104
109
|
}
|
|
105
|
-
emission = _new_emission(
|
|
110
|
+
emission = _new_emission(term_id, MODEL) | {
|
|
106
111
|
key: value for key, value in update_dict.items() if value
|
|
107
112
|
}
|
|
108
113
|
return emission
|
|
@@ -123,17 +128,24 @@ def run(cycle: dict) -> list[dict]:
|
|
|
123
128
|
A list of [Emission nodes](https://www.hestia.earth/schema/Emission) containing model results.
|
|
124
129
|
"""
|
|
125
130
|
should_run_exec = create_should_run_function(
|
|
126
|
-
_CARBON_STOCK_TERM_ID,
|
|
127
|
-
_should_compile_inventory_func,
|
|
131
|
+
carbon_stock_term_id=_CARBON_STOCK_TERM_ID,
|
|
132
|
+
should_compile_inventory_func=_should_compile_inventory_func,
|
|
133
|
+
summarise_land_use_func=summarise_land_cover_nodes,
|
|
134
|
+
detect_land_use_change_func=detect_land_cover_change,
|
|
128
135
|
measurement_method_ranking=_MEASUREMENT_METHOD_RANKING
|
|
129
136
|
)
|
|
130
137
|
|
|
131
|
-
run_exec = create_run_function(
|
|
138
|
+
run_exec = create_run_function(
|
|
139
|
+
new_emission_func=_emission,
|
|
140
|
+
land_use_change_emission_term_id=_LU_EMISSION_TERM_ID,
|
|
141
|
+
management_change_emission_term_id=_MG_EMISSION_TERM_ID
|
|
142
|
+
)
|
|
132
143
|
|
|
133
144
|
should_run, cycle_id, inventory, logs = should_run_exec(cycle)
|
|
134
145
|
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
for term_id in [_LU_EMISSION_TERM_ID, _MG_EMISSION_TERM_ID]:
|
|
147
|
+
logRequirements(cycle, model=MODEL, term=term_id, **logs)
|
|
148
|
+
logShouldRun(cycle, MODEL, term_id, should_run)
|
|
137
149
|
|
|
138
150
|
return run_exec(cycle_id, inventory) if should_run else []
|
|
139
151
|
|
|
@@ -4,6 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
|
|
|
4
4
|
from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match
|
|
5
5
|
from hestia_earth.models.utils.emission import _new_emission
|
|
6
6
|
|
|
7
|
+
from .biomass_utils import detect_land_cover_change, summarise_land_cover_nodes
|
|
7
8
|
from .co2ToAirCarbonStockChange_utils import create_run_function, create_should_run_function
|
|
8
9
|
from . import MODEL
|
|
9
10
|
|
|
@@ -40,7 +41,10 @@ RETURNS = {
|
|
|
40
41
|
"depth": "30"
|
|
41
42
|
}]
|
|
42
43
|
}
|
|
43
|
-
TERM_ID = 'co2ToAirBelowGroundBiomassStockChangeLandUseChange'
|
|
44
|
+
TERM_ID = 'co2ToAirBelowGroundBiomassStockChangeLandUseChange,co2ToAirBelowGroundBiomassStockChangeManagementChange'
|
|
45
|
+
|
|
46
|
+
_LU_EMISSION_TERM_ID = "co2ToAirBelowGroundBiomassStockChangeLandUseChange"
|
|
47
|
+
_MG_EMISSION_TERM_ID = "co2ToAirBelowGroundBiomassStockChangeManagementChange"
|
|
44
48
|
|
|
45
49
|
_DEPTH_UPPER = 0
|
|
46
50
|
_DEPTH_LOWER = 30
|
|
@@ -57,6 +61,7 @@ _SITE_TYPE_SYSTEMS_MAPPING = {
|
|
|
57
61
|
|
|
58
62
|
def _emission(
|
|
59
63
|
*,
|
|
64
|
+
term_id: str,
|
|
60
65
|
value: list[float],
|
|
61
66
|
method_tier: EmissionMethodTier,
|
|
62
67
|
sd: list[float] = None,
|
|
@@ -94,7 +99,7 @@ def _emission(
|
|
|
94
99
|
"methodTier": method_tier.value,
|
|
95
100
|
"depth": _DEPTH_LOWER
|
|
96
101
|
}
|
|
97
|
-
emission = _new_emission(
|
|
102
|
+
emission = _new_emission(term_id, MODEL) | {
|
|
98
103
|
key: value for key, value in update_dict.items() if value
|
|
99
104
|
}
|
|
100
105
|
return emission
|
|
@@ -115,17 +120,24 @@ def run(cycle: dict) -> list[dict]:
|
|
|
115
120
|
A list of [Emission nodes](https://www.hestia.earth/schema/Emission) containing model results.
|
|
116
121
|
"""
|
|
117
122
|
should_run_exec = create_should_run_function(
|
|
118
|
-
_CARBON_STOCK_TERM_ID,
|
|
119
|
-
_should_compile_inventory_func,
|
|
123
|
+
carbon_stock_term_id=_CARBON_STOCK_TERM_ID,
|
|
124
|
+
should_compile_inventory_func=_should_compile_inventory_func,
|
|
125
|
+
summarise_land_use_func=summarise_land_cover_nodes,
|
|
126
|
+
detect_land_use_change_func=detect_land_cover_change,
|
|
120
127
|
should_run_measurement_func=_should_run_measurement_func
|
|
121
128
|
)
|
|
122
129
|
|
|
123
|
-
run_exec = create_run_function(
|
|
130
|
+
run_exec = create_run_function(
|
|
131
|
+
new_emission_func=_emission,
|
|
132
|
+
land_use_change_emission_term_id=_LU_EMISSION_TERM_ID,
|
|
133
|
+
management_change_emission_term_id=_MG_EMISSION_TERM_ID
|
|
134
|
+
)
|
|
124
135
|
|
|
125
136
|
should_run, cycle_id, inventory, logs = should_run_exec(cycle)
|
|
126
137
|
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
for term_id in [_LU_EMISSION_TERM_ID, _MG_EMISSION_TERM_ID]:
|
|
139
|
+
logRequirements(cycle, model=MODEL, term=term_id, **logs)
|
|
140
|
+
logShouldRun(cycle, MODEL, term_id, should_run)
|
|
129
141
|
|
|
130
142
|
return run_exec(cycle_id, inventory) if should_run else []
|
|
131
143
|
|