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.

Files changed (125) hide show
  1. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +175 -0
  2. hestia_earth/models/cml2001Baseline/abioticResourceDepletionMineralsAndMetals.py +136 -0
  3. hestia_earth/models/cml2001Baseline/eutrophicationPotentialExcludingFate.py +2 -2
  4. hestia_earth/models/cml2001Baseline/terrestrialAcidificationPotentialIncludingFateAverageEurope.py +2 -2
  5. hestia_earth/models/cml2001NonBaseline/eutrophicationPotentialIncludingFateAverageEurope.py +2 -2
  6. hestia_earth/models/cml2001NonBaseline/terrestrialAcidificationPotentialExcludingFate.py +2 -2
  7. hestia_earth/models/cycle/completeness/cropResidue.py +15 -10
  8. hestia_earth/models/cycle/completeness/freshForage.py +60 -0
  9. hestia_earth/models/edip2003/ozoneDepletionPotential.py +2 -2
  10. hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandTransformation.py +2 -2
  11. hestia_earth/models/fantkeEtAl2016/damageToHumanHealthParticulateMatterFormation.py +7 -17
  12. hestia_earth/models/ipcc2013ExcludingFeedbacks/gwp100.py +2 -2
  13. hestia_earth/models/ipcc2013IncludingFeedbacks/gwp100.py +2 -2
  14. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +31 -243
  15. hestia_earth/models/ipcc2019/belowGroundBiomass.py +529 -0
  16. hestia_earth/models/ipcc2019/biomass_utils.py +406 -0
  17. hestia_earth/models/ipcc2019/{co2ToAirAboveGroundBiomassStockChangeLandUseChange.py → co2ToAirAboveGroundBiomassStockChange.py} +19 -7
  18. hestia_earth/models/ipcc2019/{co2ToAirBelowGroundBiomassStockChangeLandUseChange.py → co2ToAirBelowGroundBiomassStockChange.py} +19 -7
  19. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +402 -73
  20. hestia_earth/models/ipcc2019/{co2ToAirSoilOrganicCarbonStockChangeManagementChange.py → co2ToAirSoilOrganicCarbonStockChange.py} +20 -8
  21. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +3 -1
  22. hestia_earth/models/ipcc2019/pastureGrass_utils.py +6 -7
  23. hestia_earth/models/ipcc2021/gwp100.py +2 -2
  24. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsClimateChange.py +2 -2
  25. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  26. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  27. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthClimateChange.py +2 -2
  28. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
  29. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  30. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  31. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthWaterStress.py +2 -2
  32. hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  33. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  34. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  35. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  36. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsClimateChange.py +2 -2
  37. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  38. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  39. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
  40. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
  41. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  42. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  43. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
  44. hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  45. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  46. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  47. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  48. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  49. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  50. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthClimateChange.py +2 -2
  51. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
  52. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  53. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  54. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthWaterStress.py +2 -2
  55. hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  56. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  57. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  58. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  59. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  60. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  61. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
  62. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
  63. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  64. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  65. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
  66. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  67. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  68. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  69. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  70. hestia_earth/models/mocking/build_mock_search.py +44 -0
  71. hestia_earth/models/mocking/mock_search.py +8 -49
  72. hestia_earth/models/mocking/search-results.json +3055 -558
  73. hestia_earth/models/poschEtAl2008/terrestrialAcidificationPotentialAccumulatedExceedance.py +3 -3
  74. hestia_earth/models/poschEtAl2008/terrestrialEutrophicationPotentialAccumulatedExceedance.py +3 -3
  75. hestia_earth/models/preload_requests.py +1 -1
  76. hestia_earth/models/recipe2016Egalitarian/ecosystemDamageOzoneFormation.py +2 -2
  77. hestia_earth/models/recipe2016Egalitarian/freshwaterEutrophicationPotential.py +2 -2
  78. hestia_earth/models/recipe2016Egalitarian/humanDamageOzoneFormation.py +2 -2
  79. hestia_earth/models/recipe2016Egalitarian/marineEutrophicationPotential.py +2 -2
  80. hestia_earth/models/recipe2016Egalitarian/ozoneDepletionPotential.py +2 -2
  81. hestia_earth/models/recipe2016Egalitarian/terrestrialAcidificationPotential.py +2 -2
  82. hestia_earth/models/recipe2016Hierarchist/ecosystemDamageOzoneFormation.py +2 -2
  83. hestia_earth/models/recipe2016Hierarchist/freshwaterEutrophicationPotential.py +2 -2
  84. hestia_earth/models/recipe2016Hierarchist/humanDamageOzoneFormation.py +2 -2
  85. hestia_earth/models/recipe2016Hierarchist/marineEutrophicationPotential.py +2 -2
  86. hestia_earth/models/recipe2016Hierarchist/ozoneDepletionPotential.py +2 -2
  87. hestia_earth/models/recipe2016Hierarchist/terrestrialAcidificationPotential.py +2 -2
  88. hestia_earth/models/recipe2016Individualist/ecosystemDamageOzoneFormation.py +2 -2
  89. hestia_earth/models/recipe2016Individualist/freshwaterEutrophicationPotential.py +2 -2
  90. hestia_earth/models/recipe2016Individualist/humanDamageOzoneFormation.py +2 -2
  91. hestia_earth/models/recipe2016Individualist/marineEutrophicationPotential.py +2 -2
  92. hestia_earth/models/recipe2016Individualist/ozoneDepletionPotential.py +2 -2
  93. hestia_earth/models/recipe2016Individualist/terrestrialAcidificationPotential.py +2 -2
  94. hestia_earth/models/schmidt2007/utils.py +13 -4
  95. hestia_earth/models/utils/blank_node.py +73 -3
  96. hestia_earth/models/utils/constant.py +8 -1
  97. hestia_earth/models/utils/cycle.py +10 -13
  98. hestia_earth/models/utils/fuel.py +1 -1
  99. hestia_earth/models/utils/impact_assessment.py +49 -24
  100. hestia_earth/models/utils/lookup.py +36 -7
  101. hestia_earth/models/utils/pesticideAI.py +1 -1
  102. hestia_earth/models/utils/property.py +11 -4
  103. hestia_earth/models/utils/term.py +15 -8
  104. hestia_earth/models/version.py +1 -1
  105. {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/METADATA +2 -2
  106. {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/RECORD +123 -114
  107. {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/WHEEL +1 -1
  108. tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +196 -0
  109. tests/models/cml2001Baseline/test_abioticResourceDepletionMineralsAndMetals.py +124 -0
  110. tests/models/cycle/completeness/test_freshForage.py +21 -0
  111. tests/models/edip2003/test_ozoneDepletionPotential.py +1 -13
  112. tests/models/environmentalFootprintV3/test_soilQualityIndexLandTransformation.py +1 -2
  113. tests/models/impact_assessment/test_emissions.py +1 -0
  114. tests/models/ipcc2019/test_aboveGroundBiomass.py +27 -63
  115. tests/models/ipcc2019/test_belowGroundBiomass.py +146 -0
  116. tests/models/ipcc2019/test_biomass_utils.py +115 -0
  117. tests/models/ipcc2019/{test_co2ToAirAboveGroundBiomassStockChangeLandUseChange.py → test_co2ToAirAboveGroundBiomassStockChange.py} +5 -5
  118. tests/models/ipcc2019/{test_co2ToAirBelowGroundBiomassStockChangeLandUseChange.py → test_co2ToAirBelowGroundBiomassStockChange.py} +5 -5
  119. tests/models/ipcc2019/{test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py → test_co2ToAirSoilOrganicCarbonStockChange.py} +5 -5
  120. tests/models/ipcc2021/test_gwp100.py +2 -2
  121. tests/models/utils/test_impact_assessment.py +3 -3
  122. hestia_earth/models/ipcc2019/aboveGroundBiomass_utils.py +0 -180
  123. tests/models/ipcc2019/test_aboveGroundBiomass_utils.py +0 -92
  124. {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.dist-info}/LICENSE +0 -0
  125. {hestia_earth_models-0.64.9.dist-info → hestia_earth_models-0.64.11.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(TERM_ID, MODEL) | {
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(_emission)
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
- logRequirements(cycle, model=MODEL, term=TERM_ID, **logs)
136
- logShouldRun(cycle, MODEL, TERM_ID, should_run)
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(TERM_ID, MODEL) | {
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(_emission)
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
- logRequirements(cycle, model=MODEL, term=TERM_ID, **logs)
128
- logShouldRun(cycle, MODEL, TERM_ID, should_run)
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