hestia-earth-models 0.61.7__py3-none-any.whl → 0.62.0__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 (51) hide show
  1. hestia_earth/models/cycle/completeness/electricityFuel.py +60 -0
  2. hestia_earth/models/cycle/product/economicValueShare.py +47 -31
  3. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +44 -59
  4. hestia_earth/models/geospatialDatabase/histosol.py +4 -0
  5. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +4 -2
  6. hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +1 -1
  7. hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +1 -1
  8. hestia_earth/models/ipcc2019/animal/pastureGrass.py +30 -24
  9. hestia_earth/models/ipcc2019/belowGroundCropResidue.py +1 -1
  10. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -1
  11. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +511 -458
  12. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +5 -1
  13. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +116 -3882
  14. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2060 -0
  15. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1630 -0
  16. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +324 -0
  17. hestia_earth/models/ipcc2019/pastureGrass.py +37 -19
  18. hestia_earth/models/ipcc2019/pastureGrass_utils.py +4 -21
  19. hestia_earth/models/mocking/search-results.json +293 -289
  20. hestia_earth/models/site/organicCarbonPerHa.py +58 -44
  21. hestia_earth/models/site/soilMeasurement.py +18 -13
  22. hestia_earth/models/utils/__init__.py +28 -0
  23. hestia_earth/models/utils/array_builders.py +578 -0
  24. hestia_earth/models/utils/blank_node.py +55 -39
  25. hestia_earth/models/utils/descriptive_stats.py +285 -0
  26. hestia_earth/models/utils/emission.py +73 -2
  27. hestia_earth/models/utils/inorganicFertiliser.py +2 -2
  28. hestia_earth/models/utils/measurement.py +118 -4
  29. hestia_earth/models/version.py +1 -1
  30. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/METADATA +2 -2
  31. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/RECORD +51 -39
  32. tests/models/cycle/completeness/test_electricityFuel.py +21 -0
  33. tests/models/cycle/product/test_economicValueShare.py +8 -0
  34. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +2 -2
  35. tests/models/ipcc2019/animal/test_pastureGrass.py +2 -2
  36. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +55 -165
  37. tests/models/ipcc2019/test_organicCarbonPerHa.py +219 -460
  38. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +471 -0
  39. tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +208 -0
  40. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +75 -0
  41. tests/models/ipcc2019/test_pastureGrass.py +0 -16
  42. tests/models/site/test_organicCarbonPerHa.py +3 -12
  43. tests/models/site/test_soilMeasurement.py +3 -18
  44. tests/models/utils/test_array_builders.py +253 -0
  45. tests/models/utils/test_blank_node.py +154 -15
  46. tests/models/utils/test_descriptive_stats.py +134 -0
  47. tests/models/utils/test_emission.py +51 -1
  48. tests/models/utils/test_measurement.py +54 -2
  49. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/LICENSE +0 -0
  50. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/WHEEL +0 -0
  51. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2060 @@
1
+ """
2
+ The IPCC Tier 1 methodology for estimating soil organic carbon stock changes in the 0 - 30cm depth interval due to
3
+ management changes.
4
+
5
+ The model cannot not run on Sites with polar moist (ecoClimateZone 5) or polar dry (ecoClimateZone 6).
6
+
7
+ More information on this model, including data requirements **and** recommendations, and examples can be found in the
8
+ [Hestia SOC wiki](https://gitlab.com/hestia-earth/hestia-engine-models/-/wikis/Soil-organic-carbon-modelling).
9
+
10
+ Source: [IPCC 2019, Vol. 4, Chapter 2](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html).
11
+ """
12
+
13
+ from enum import Enum
14
+ from functools import reduce
15
+ from numpy import empty_like, random, vstack
16
+ from numpy.typing import NDArray
17
+ from pydash.objects import merge
18
+ from typing import Callable, Optional, Union
19
+
20
+ from hestia_earth.schema import MeasurementMethodClassification, SiteSiteType, TermTermType
21
+ from hestia_earth.utils.model import find_term_match, filter_list_term_type
22
+
23
+ from hestia_earth.models.utils.array_builders import gen_seed
24
+ from hestia_earth.models.utils.blank_node import (
25
+ cumulative_nodes_match, cumulative_nodes_lookup_match, cumulative_nodes_term_match, get_node_value,
26
+ node_lookup_match, node_term_match, group_nodes_by_year
27
+ )
28
+ from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
29
+ from hestia_earth.models.utils.measurement import _new_measurement
30
+ from hestia_earth.models.utils.property import get_node_property
31
+
32
+ from .organicCarbonPerHa_utils import (
33
+ check_irrigation, DEPTH_LOWER, DEPTH_UPPER, EcoClimateZone,
34
+ get_cover_crop_property_terms_with_cache,
35
+ get_residue_removed_or_burnt_terms_with_cache,
36
+ get_upland_rice_land_cover_terms_with_cache,
37
+ IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE, IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE,
38
+ IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID,
39
+ IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE, IpccSoilCategory, IpccCarbonInputCategory,
40
+ IpccLandUseCategory, IpccManagementCategory, MIN_AREA_THRESHOLD, sample_constant, sample_plus_minus_error,
41
+ sample_plus_minus_uncertainty, SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY, SUPER_MAJORITY_AREA_THRESHOLD, STATS_DEFINITION
42
+ )
43
+
44
+ _LOOKUPS = {
45
+ "crop": "IPCC_LAND_USE_CATEGORY",
46
+ "landCover": [
47
+ "IPCC_LAND_USE_CATEGORY",
48
+ "LOW_RESIDUE_PRODUCING_CROP",
49
+ "N_FIXING_CROP"
50
+ ],
51
+ "landUseManagement": "PRACTICE_INCREASING_C_INPUT",
52
+ "soilType": "IPCC_SOIL_CATEGORY",
53
+ "tillage": "IPCC_TILLAGE_MANAGEMENT_CATEGORY",
54
+ "usdaSoilType": "IPCC_SOIL_CATEGORY"
55
+ }
56
+
57
+ _TERM_ID = 'organicCarbonPerHa'
58
+ _METHOD_CLASSIFICATION = MeasurementMethodClassification.TIER_1_MODEL.value
59
+
60
+ _CLAY_CONTENT_TERM_ID = "clayContent"
61
+ _SAND_CONTENT_TERM_ID = "sandContent"
62
+ _LONG_FALLOW_CROP_TERM_ID = "longFallowCrop"
63
+ _IMPROVED_PASTURE_TERM_ID = "improvedPasture"
64
+ _SHORT_BARE_FALLOW_TERM_ID = "shortBareFallow"
65
+ _ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
66
+ _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
67
+ _ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserUsed"
68
+ _SOIL_AMENDMENT_USED_TERM_ID = "amendmentIncreasingSoilCarbonUsed"
69
+
70
+ _CLAY_CONTENT_MAX = 8
71
+ _SAND_CONTENT_MIN = 70
72
+
73
+ _EQUILIBRIUM_TRANSITION_PERIOD = 20
74
+ """
75
+ The number of years required for soil organic carbon to reach equilibrium after
76
+ a change in land use, management regime or carbon input regime.
77
+ """
78
+
79
+ _EXCLUDED_ECO_CLIMATE_ZONES = {EcoClimateZone.POLAR_MOIST, EcoClimateZone.POLAR_DRY}
80
+
81
+ _VALID_SITE_TYPES = {
82
+ SiteSiteType.CROPLAND.value,
83
+ SiteSiteType.FOREST.value,
84
+ SiteSiteType.OTHER_NATURAL_VEGETATION.value,
85
+ SiteSiteType.PERMANENT_PASTURE.value
86
+ }
87
+
88
+
89
+ def _measurement(
90
+ timestamps: list[int],
91
+ descriptive_stats_dict: dict
92
+ ) -> dict:
93
+ """
94
+ Build a Hestia `Measurement` node to contain a value and descriptive statistics calculated by the models.
95
+
96
+ The `descriptive_stats_dict` parameter should include the following keys and values from the
97
+ [Measurement](https://www-staging.hestia.earth/schema/Measurement) schema:
98
+ ```
99
+ {
100
+ "value": list[float],
101
+ "sd": list[float],
102
+ "min": list[float],
103
+ "max": list[float],
104
+ "statsDefinition": str,
105
+ "observations": list[int]
106
+ }
107
+ ```
108
+
109
+ Parameters
110
+ ----------
111
+ timestamps : list[int]
112
+ A list of calendar years associated to the calculated SOC stocks.
113
+ descriptive_stats_dict : dict
114
+ A dict containing the descriptive statistics data that should be added to the node.
115
+
116
+ Returns
117
+ -------
118
+ dict
119
+ A valid Hestia `Measurement` node, see: https://www.hestia.earth/schema/Measurement.
120
+ """
121
+ measurement = _new_measurement(_TERM_ID) | descriptive_stats_dict
122
+ measurement["dates"] = [f"{year}-12-31" for year in timestamps]
123
+ measurement["depthUpper"] = DEPTH_UPPER
124
+ measurement["depthLower"] = DEPTH_LOWER
125
+ measurement["methodClassification"] = _METHOD_CLASSIFICATION
126
+ return measurement
127
+
128
+
129
+ class _InventoryKey(Enum):
130
+ """
131
+ Enum representing the inner keys of the annual inventory is constructed from `Site` data.
132
+ """
133
+ LU_CATEGORY = "ipcc-land-use-category"
134
+ MG_CATEGORY = "ipcc-management-category"
135
+ CI_CATEGORY = "ipcc-carbon-input-category"
136
+ SHOULD_RUN = "should-run-tier-1"
137
+
138
+
139
+ _REQUIRED_KEYS = {
140
+ _InventoryKey.LU_CATEGORY,
141
+ _InventoryKey.MG_CATEGORY,
142
+ _InventoryKey.CI_CATEGORY
143
+ }
144
+ """
145
+ The `_InventoryKey`s that must have valid values for an inventory year to be included in the model.
146
+ """
147
+
148
+
149
+ _SOC_REFS = {
150
+ IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: {
151
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 64000, "uncertainty": 5, "observations": 489},
152
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 24000, "uncertainty": 5, "observations": 781},
153
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 81000, "uncertainty": 5, "observations": 334},
154
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 43000, "uncertainty": 8, "observations": 177},
155
+ EcoClimateZone.POLAR_MOIST: {"value": 59000, "uncertainty": 41, "observations": 24},
156
+ EcoClimateZone.POLAR_DRY: {"value": 59000, "uncertainty": 41, "observations": 24},
157
+ EcoClimateZone.BOREAL_MOIST: {"value": 63000, "uncertainty": 18, "observations": 35},
158
+ EcoClimateZone.BOREAL_DRY: {"value": 63000, "uncertainty": 18, "observations": 35},
159
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 51000, "uncertainty": 10, "observations": 114},
160
+ EcoClimateZone.TROPICAL_WET: {"value": 60000, "uncertainty": 8, "observations": 137},
161
+ EcoClimateZone.TROPICAL_MOIST: {"value": 40000, "uncertainty": 7, "observations": 226},
162
+ EcoClimateZone.TROPICAL_DRY: {"value": 21000, "uncertainty": 5, "observations": 554}
163
+ },
164
+ IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: {
165
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 55000, "uncertainty": 8, "observations": 183},
166
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 19000, "uncertainty": 16, "observations": 41},
167
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 76000, "uncertainty": 51, "observations": 6},
168
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 33000, "uncertainty": 90},
169
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 44000, "uncertainty": 11, "observations": 84},
170
+ EcoClimateZone.TROPICAL_WET: {"value": 52000, "uncertainty": 6, "observations": 271},
171
+ EcoClimateZone.TROPICAL_MOIST: {"value": 38000, "uncertainty": 5, "observations": 326},
172
+ EcoClimateZone.TROPICAL_DRY: {"value": 19000, "uncertainty": 10, "observations": 135}
173
+ },
174
+ IpccSoilCategory.SANDY_SOILS: {
175
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 36000, "uncertainty": 23, "observations": 39},
176
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 10000, "uncertainty": 5, "observations": 338},
177
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 81000, "uncertainty": 5, "observations": 334},
178
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 51000, "uncertainty": 13, "observations": 126},
179
+ EcoClimateZone.POLAR_MOIST: {"value": 27000, "uncertainty": 67, "observations": 18},
180
+ EcoClimateZone.POLAR_DRY: {"value": 27000, "uncertainty": 67, "observations": 18},
181
+ EcoClimateZone.BOREAL_MOIST: {"value": 10000, "uncertainty": 90},
182
+ EcoClimateZone.BOREAL_DRY: {"value": 10000, "uncertainty": 90},
183
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 52000, "uncertainty": 34, "observations": 11},
184
+ EcoClimateZone.TROPICAL_WET: {"value": 46000, "uncertainty": 20, "observations": 43},
185
+ EcoClimateZone.TROPICAL_MOIST: {"value": 27, "uncertainty": 12, "observations": 76},
186
+ EcoClimateZone.TROPICAL_DRY: {"value": 9000, "uncertainty": 9, "observations": 164}
187
+ },
188
+ IpccSoilCategory.SPODIC_SOILS: {
189
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 143000, "uncertainty": 30, "observations": 9},
190
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 128000, "uncertainty": 14, "observations": 45},
191
+ EcoClimateZone.BOREAL_MOIST: {"value": 117000, "uncertainty": 90},
192
+ EcoClimateZone.BOREAL_DRY: {"value": 117000, "uncertainty": 90}
193
+ },
194
+ IpccSoilCategory.VOLCANIC_SOILS: {
195
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 138000, "uncertainty": 12, "observations": 42},
196
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 84000, "uncertainty": 65, "observations": 10},
197
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 136000, "uncertainty": 14, "observations": 28},
198
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 20000, "uncertainty": 90},
199
+ EcoClimateZone.BOREAL_MOIST: {"value": 20000, "uncertainty": 90},
200
+ EcoClimateZone.BOREAL_DRY: {"value": 20000, "uncertainty": 90},
201
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 96000, "uncertainty": 31, "observations": 10},
202
+ EcoClimateZone.TROPICAL_WET: {"value": 77000, "uncertainty": 27, "observations": 14},
203
+ EcoClimateZone.TROPICAL_MOIST: {"value": 70000, "uncertainty": 90},
204
+ EcoClimateZone.TROPICAL_DRY: {"value": 50000, "uncertainty": 90}
205
+ },
206
+ IpccSoilCategory.WETLAND_SOILS: {
207
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 135000, "uncertainty": 28, "observations": 28},
208
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 74000, "uncertainty": 17, "observations": 49},
209
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 128000, "uncertainty": 13, "observations": 42},
210
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 87000, "uncertainty": 90},
211
+ EcoClimateZone.BOREAL_MOIST: {"value": 116000, "uncertainty": 65, "observations": 6},
212
+ EcoClimateZone.BOREAL_DRY: {"value": 116000, "uncertainty": 65, "observations": 6},
213
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 82000, "uncertainty": 50, "observations": 12},
214
+ EcoClimateZone.TROPICAL_WET: {"value": 49000, "uncertainty": 19, "observations": 33},
215
+ EcoClimateZone.TROPICAL_MOIST: {"value": 68000, "uncertainty": 17, "observations": 55},
216
+ EcoClimateZone.TROPICAL_DRY: {"value": 22000, "uncertainty": 17, "observations": 32}
217
+ }
218
+ }
219
+
220
+ _LAND_USE_FACTORS = {
221
+ IpccLandUseCategory.GRASSLAND: {
222
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
223
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
224
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
225
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
226
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
227
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
228
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
229
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
230
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
231
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
232
+ },
233
+ IpccLandUseCategory.PERENNIAL_CROPS: {
234
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.72, "error": 22},
235
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.72, "error": 22},
236
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.72, "error": 22},
237
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.72, "error": 22},
238
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.72, "error": 22},
239
+ EcoClimateZone.BOREAL_DRY: {"value": 0.72, "error": 22},
240
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.1, "error": 50},
241
+ EcoClimateZone.TROPICAL_WET: {"value": 1.1, "error": 25},
242
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.1, "error": 25},
243
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.1, "error": 25}
244
+ },
245
+ IpccLandUseCategory.PADDY_RICE_CULTIVATION: {
246
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.35, "error": 4},
247
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1.35, "error": 4},
248
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.35, "error": 4},
249
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1.35, "error": 4},
250
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.35, "error": 4},
251
+ EcoClimateZone.BOREAL_DRY: {"value": 1.35, "error": 4},
252
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.35, "error": 4},
253
+ EcoClimateZone.TROPICAL_WET: {"value": 1.35, "error": 4},
254
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.35, "error": 4},
255
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.35, "error": 4}
256
+ },
257
+ IpccLandUseCategory.ANNUAL_CROPS_WET: {
258
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.71, "error": 41},
259
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.71, "error": 41},
260
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.71, "error": 41},
261
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.71, "error": 41},
262
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.71, "error": 41},
263
+ EcoClimateZone.BOREAL_DRY: {"value": 0.71, "error": 41},
264
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 0.86, "error": 50},
265
+ EcoClimateZone.TROPICAL_WET: {"value": 0.83, "error": 11},
266
+ EcoClimateZone.TROPICAL_MOIST: {"value": 0.83, "error": 11},
267
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.92, "error": 13}
268
+ },
269
+ IpccLandUseCategory.ANNUAL_CROPS: {
270
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.69, "error": 16},
271
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.76, "error": 12},
272
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.70, "error": 12},
273
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.77, "error": 14},
274
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.70, "error": 12},
275
+ EcoClimateZone.BOREAL_DRY: {"value": 0.77, "error": 14},
276
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 0.86, "error": 50},
277
+ EcoClimateZone.TROPICAL_WET: {"value": 0.83, "error": 11},
278
+ EcoClimateZone.TROPICAL_MOIST: {"value": 0.83, "error": 11},
279
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.92, "error": 13}
280
+ },
281
+ IpccLandUseCategory.SET_ASIDE: {
282
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.82, "error": 17},
283
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.93, "error": 11},
284
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.82, "error": 17},
285
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.93, "error": 11},
286
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.82, "error": 17},
287
+ EcoClimateZone.BOREAL_DRY: {"value": 0.93, "error": 11},
288
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 0.88, "error": 50},
289
+ EcoClimateZone.TROPICAL_WET: {"value": 0.82, "error": 17},
290
+ EcoClimateZone.TROPICAL_MOIST: {"value": 0.82, "error": 17},
291
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.93, "error": 11}
292
+ },
293
+ IpccLandUseCategory.FOREST: {
294
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
295
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
296
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
297
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
298
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
299
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
300
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
301
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
302
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
303
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
304
+ },
305
+ IpccLandUseCategory.NATIVE: {
306
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
307
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
308
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
309
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
310
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
311
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
312
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
313
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
314
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
315
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
316
+ },
317
+ IpccLandUseCategory.OTHER: {
318
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
319
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
320
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
321
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
322
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
323
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
324
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
325
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
326
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
327
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
328
+ }
329
+ }
330
+
331
+ _MANAGEMENT_FACTORS = {
332
+ IpccManagementCategory.SEVERELY_DEGRADED: {
333
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.7, "error": 40},
334
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.7, "error": 40},
335
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.7, "error": 40},
336
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.7, "error": 40},
337
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.7, "error": 40},
338
+ EcoClimateZone.BOREAL_DRY: {"value": 0.7, "error": 40},
339
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 0.7, "error": 40},
340
+ EcoClimateZone.TROPICAL_WET: {"value": 0.7, "error": 40},
341
+ EcoClimateZone.TROPICAL_MOIST: {"value": 0.7, "error": 40},
342
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.7, "error": 40}
343
+ },
344
+ IpccManagementCategory.IMPROVED_GRASSLAND: {
345
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.14, "error": 11},
346
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1.14, "error": 11},
347
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.14, "error": 11},
348
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1.14, "error": 11},
349
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.14, "error": 11},
350
+ EcoClimateZone.BOREAL_DRY: {"value": 1.14, "error": 11},
351
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.16, "error": 40},
352
+ EcoClimateZone.TROPICAL_WET: {"value": 1.17, "error": 9},
353
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.17, "error": 9},
354
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.17, "error": 9}
355
+ },
356
+ IpccManagementCategory.HIGH_INTENSITY_GRAZING: {
357
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.9, "error": 8},
358
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.9, "error": 8},
359
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.9, "error": 8},
360
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.9, "error": 8},
361
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.9, "error": 8},
362
+ EcoClimateZone.BOREAL_DRY: {"value": 0.9, "error": 8},
363
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 0.9, "error": 8},
364
+ EcoClimateZone.TROPICAL_WET: {"value": 0.9, "error": 8},
365
+ EcoClimateZone.TROPICAL_MOIST: {"value": 0.9, "error": 8},
366
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.9, "error": 8}
367
+ },
368
+ IpccManagementCategory.NOMINALLY_MANAGED: {
369
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
370
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
371
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
372
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
373
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
374
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
375
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
376
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
377
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
378
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
379
+ },
380
+ IpccManagementCategory.FULL_TILLAGE: {
381
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
382
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
383
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
384
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
385
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
386
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
387
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
388
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
389
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
390
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
391
+ },
392
+ IpccManagementCategory.REDUCED_TILLAGE: {
393
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.05, "error": 4},
394
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.99, "error": 3},
395
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.04, "error": 4},
396
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.98, "error": 5},
397
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.04, "error": 4},
398
+ EcoClimateZone.BOREAL_DRY: {"value": 0.98, "error": 5},
399
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.02, "error": 50},
400
+ EcoClimateZone.TROPICAL_WET: {"value": 1.04, "error": 7},
401
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.04, "error": 7},
402
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.99, "error": 7}
403
+ },
404
+ IpccManagementCategory.NO_TILLAGE: {
405
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.1, "error": 4},
406
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1.04, "error": 3},
407
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.09, "error": 4},
408
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1.03, "error": 4},
409
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.09, "error": 4},
410
+ EcoClimateZone.BOREAL_DRY: {"value": 1.03, "error": 4},
411
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.08, "error": 50},
412
+ EcoClimateZone.TROPICAL_WET: {"value": 1.1, "error": 5},
413
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.1, "error": 5},
414
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.04, "error": 7}
415
+ },
416
+ IpccManagementCategory.OTHER: {
417
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
418
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
419
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
420
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
421
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
422
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
423
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
424
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
425
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
426
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
427
+ }
428
+ }
429
+
430
+ _CARBON_INPUT_FACTORS = {
431
+ IpccCarbonInputCategory.GRASSLAND_HIGH: {
432
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.11, "error": 7},
433
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1.11, "error": 7},
434
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.11, "error": 7},
435
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1.11, "error": 7},
436
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.11, "error": 7},
437
+ EcoClimateZone.BOREAL_DRY: {"value": 1.11, "error": 7},
438
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.11, "error": 7},
439
+ EcoClimateZone.TROPICAL_WET: {"value": 1.11, "error": 7},
440
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.11, "error": 7},
441
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.11, "error": 7}
442
+ },
443
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM: {
444
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
445
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
446
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
447
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
448
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
449
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
450
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
451
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
452
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
453
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
454
+ },
455
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE: {
456
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.44, "error": 13},
457
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1.37, "error": 12},
458
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.44, "error": 13},
459
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1.37, "error": 12},
460
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.44, "error": 13},
461
+ EcoClimateZone.BOREAL_DRY: {"value": 1.37, "error": 12},
462
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.41, "error": 50},
463
+ EcoClimateZone.TROPICAL_WET: {"value": 1.44, "error": 13},
464
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.44, "error": 13},
465
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.37, "error": 12}
466
+ },
467
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE: {
468
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1.11, "error": 10},
469
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1.04, "error": 13},
470
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1.11, "error": 10},
471
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1.04, "error": 13},
472
+ EcoClimateZone.BOREAL_MOIST: {"value": 1.11, "error": 10},
473
+ EcoClimateZone.BOREAL_DRY: {"value": 1.04, "error": 13},
474
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1.08, "error": 50},
475
+ EcoClimateZone.TROPICAL_WET: {"value": 1.11, "error": 10},
476
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1.11, "error": 10},
477
+ EcoClimateZone.TROPICAL_DRY: {"value": 1.04, "error": 13}
478
+ },
479
+ IpccCarbonInputCategory.CROPLAND_MEDIUM: {
480
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
481
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
482
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
483
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
484
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
485
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
486
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
487
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
488
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
489
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
490
+ },
491
+ IpccCarbonInputCategory.CROPLAND_LOW: {
492
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 0.92, "error": 14},
493
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 0.95, "error": 13},
494
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 0.92, "error": 14},
495
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 0.95, "error": 13},
496
+ EcoClimateZone.BOREAL_MOIST: {"value": 0.92, "error": 14},
497
+ EcoClimateZone.BOREAL_DRY: {"value": 0.95, "error": 13},
498
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 0.94, "error": 50},
499
+ EcoClimateZone.TROPICAL_WET: {"value": 0.92, "error": 14},
500
+ EcoClimateZone.TROPICAL_MOIST: {"value": 0.92, "error": 14},
501
+ EcoClimateZone.TROPICAL_DRY: {"value": 0.95, "error": 13}
502
+ },
503
+ IpccCarbonInputCategory.OTHER: {
504
+ EcoClimateZone.WARM_TEMPERATE_MOIST: {"value": 1},
505
+ EcoClimateZone.WARM_TEMPERATE_DRY: {"value": 1},
506
+ EcoClimateZone.COOL_TEMPERATE_MOIST: {"value": 1},
507
+ EcoClimateZone.COOL_TEMPERATE_DRY: {"value": 1},
508
+ EcoClimateZone.BOREAL_MOIST: {"value": 1},
509
+ EcoClimateZone.BOREAL_DRY: {"value": 1},
510
+ EcoClimateZone.TROPICAL_MONTANE: {"value": 1},
511
+ EcoClimateZone.TROPICAL_WET: {"value": 1},
512
+ EcoClimateZone.TROPICAL_MOIST: {"value": 1},
513
+ EcoClimateZone.TROPICAL_DRY: {"value": 1}
514
+ }
515
+ }
516
+
517
+ _KWARGS_TO_SAMPLE_FUNC = {
518
+ ("value", "uncertainty"): sample_plus_minus_uncertainty,
519
+ ("value", "error"): sample_plus_minus_error,
520
+ ("value",): sample_constant
521
+ }
522
+
523
+ _IPCC_CATEGORY_TO_FACTOR_DICT = {
524
+ IpccSoilCategory: _SOC_REFS,
525
+ IpccLandUseCategory: _LAND_USE_FACTORS,
526
+ IpccManagementCategory: _MANAGEMENT_FACTORS,
527
+ IpccCarbonInputCategory: _CARBON_INPUT_FACTORS
528
+ }
529
+
530
+
531
+ def _sample_parameter(
532
+ iterations: int,
533
+ parameter: Union[IpccSoilCategory, IpccLandUseCategory, IpccManagementCategory, IpccCarbonInputCategory],
534
+ eco_climate_zone: EcoClimateZone,
535
+ seed: Union[int, random.Generator, None] = None
536
+ ) -> NDArray:
537
+ """
538
+ Sample a model parameter (SOC ref or stock change factor) using the function specified in `KWARGS_TO_SAMPLE_FUNC`.
539
+
540
+ Parameters
541
+ ----------
542
+ iterations : int
543
+ The number of samples to take.
544
+ parameter : IpccSoilCategory | IpccLandUseCategory | IpccManagementCategory | IpccCarbonInputCategory
545
+ The model parameter to sample.
546
+ eco_climate_zone : EcoClimateZone
547
+ The eco-climate zone of the site.
548
+ seed : int | Generator | None, optional
549
+ A seed to initialize the BitGenerator. If passed a Generator, it will be returned unaltered. If `None`, then
550
+ fresh, unpredictable entropy will be pulled from the OS.
551
+
552
+ Returns
553
+ -------
554
+ NDArray
555
+ The sampled parameter as a numpy array with shape `(1, iterations)`.
556
+ """
557
+ parameter_dict = _IPCC_CATEGORY_TO_FACTOR_DICT.get(type(parameter))
558
+ kwargs = parameter_dict.get(parameter, {}).get(eco_climate_zone, {})
559
+ func = _get_sample_func(kwargs)
560
+ return func(iterations=iterations, seed=seed, **kwargs)
561
+
562
+
563
+ def _get_sample_func(kwargs: dict) -> Callable:
564
+ """
565
+ Select the correct sample function for a parameter based on the distribution data available. All possible
566
+ parameters for the model should have, at a minimum, a `value`, meaning that no default function needs to be
567
+ specified.
568
+
569
+ This function has been extracted into it's own method to allow for mocking of sample function.
570
+
571
+ Keyword Args
572
+ ------------
573
+ value : float
574
+ The distribution mean.
575
+ uncertainty : float
576
+ The +/- uncertainty of the 95% confidence interval expressed as a percentge of the mean.
577
+ error : float
578
+ Two standard deviations expressed as a percentage of the mean.
579
+
580
+ Returns
581
+ -------
582
+ Callable
583
+ The sample function for the distribution.
584
+ """
585
+ return next(
586
+ sample_func for required_kwargs, sample_func in _KWARGS_TO_SAMPLE_FUNC.items()
587
+ if all(kwarg in kwargs.keys() for kwarg in required_kwargs)
588
+ )
589
+
590
+
591
+ def _get_soc_ref_preview(ipcc_soil_category: IpccSoilCategory, eco_climate_zone: EcoClimateZone) -> Union[float, None]:
592
+ """
593
+ Retrieve the mean value of the SOC ref for a specific combination of `IpccSoilCategory` and `EcoClimateZone`. This
594
+ is primarily for logging purposes.
595
+
596
+ Parameters
597
+ ----------
598
+ ipcc_soil_category : IpccSoilCategory
599
+ eco_climate_zone: EcoClimateZone
600
+
601
+ Returns
602
+ -------
603
+ float | None
604
+ The mean value SOC ref or `None` if no reference value is available.
605
+ """
606
+ return _SOC_REFS.get(ipcc_soil_category, {}).get(eco_climate_zone, {}).get("value", None)
607
+
608
+
609
+ # --- TIER 1 MODEL ---
610
+
611
+
612
+ def should_run(site: dict) -> tuple[bool, dict, dict]:
613
+ """
614
+ Extract data from site & related cycles, pre-process data and determine whether there is sufficient data to run the
615
+ Tier 1 model.
616
+
617
+ The returned `inventory` should be a dict with the shape:
618
+ ```
619
+ {
620
+ year (int): {
621
+ _InventoryKey.SHOULD_RUN: bool,
622
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory,
623
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory,
624
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory
625
+ },
626
+ ...
627
+ }
628
+ ```
629
+
630
+ The returned `kwargs` should be a dict with the shape:
631
+ ```
632
+ {
633
+ "eco_climate_zone": int,
634
+ "ipcc_soil_category": IpccSoilCategory,
635
+ "soc_ref": float
636
+ }
637
+ ```
638
+
639
+ Parameters
640
+ ----------
641
+ site : dict
642
+ A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
643
+
644
+ Returns
645
+ -------
646
+ tuple[bool, dict, dict]
647
+ A tuple containing `(should_run_, inventory, kwargs)`.
648
+ """
649
+ site_type = site.get("siteType", "")
650
+ management_nodes = site.get("management", [])
651
+ measurement_nodes = site.get("measurements", [])
652
+
653
+ has_management = len(management_nodes) > 0
654
+ has_measurements = len(measurement_nodes) > 0
655
+
656
+ should_compile_inventory = all([
657
+ site_type in _VALID_SITE_TYPES,
658
+ has_management,
659
+ has_measurements
660
+ ])
661
+
662
+ inventory, kwargs = (
663
+ _compile_inventory(site_type, management_nodes, measurement_nodes)
664
+ if should_compile_inventory else ({}, {})
665
+ )
666
+ kwargs["seed"] = gen_seed(site)
667
+
668
+ should_run_ = all([
669
+ kwargs.get("eco_climate_zone") not in _EXCLUDED_ECO_CLIMATE_ZONES,
670
+ (kwargs.get("soc_ref") or -9999) > 0,
671
+ any(year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN))
672
+ ])
673
+
674
+ logs = {
675
+ "site_type": site_type,
676
+ "has_management": has_management,
677
+ "has_measurements": has_measurements,
678
+ "should_compile_inventory_tier_1": should_compile_inventory,
679
+ "should_run_tier_1": should_run_
680
+ }
681
+
682
+ return should_run_, inventory, kwargs, logs
683
+
684
+
685
+ def run(
686
+ inventory: dict,
687
+ *,
688
+ eco_climate_zone: EcoClimateZone,
689
+ ipcc_soil_category: IpccSoilCategory,
690
+ iterations: int,
691
+ seed: Union[int, random.Generator, None] = None,
692
+ **_
693
+ ) -> list[dict]:
694
+ """
695
+ Run the IPCC (2019) Tier 1 methodology for calculating SOC stocks (in kg C ha-1) for each year in the inventory
696
+ and wrap each of the calculated values in Hestia measurement nodes. To avoid any errors, the `inventory` parameter
697
+ must be pre-validated by the `should_run` function.
698
+
699
+ See [IPCC (2019) Vol. 4, Ch. 2](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
700
+
701
+ The inventory should be in the following shape:
702
+ ```
703
+ {
704
+ year (int): {
705
+ _InventoryKey.SHOULD_RUN: bool,
706
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory,
707
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory,
708
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory
709
+ },
710
+ ...
711
+ }
712
+ ```
713
+
714
+ Parameters
715
+ ----------
716
+ inventory : dict
717
+ The inventory built by the `_should_run` function.
718
+ eco_climate_zone : EcoClimateZone
719
+ The eco-climate zone of the site.
720
+ ipcc_soil_category : IpccSoilCategory
721
+ The IPCC soil category of the site.
722
+ iterations : int
723
+ Number of iterations to run the model for.
724
+ seed : int | Generator | None, optional
725
+ A seed to initialize the BitGenerator. If passed a Generator, it will be returned unaltered. If `None`, then
726
+ fresh, unpredictable entropy will be pulled from the OS.
727
+
728
+ Returns
729
+ -------
730
+ list[dict]
731
+ A list of HESTIA nodes containing model output results.
732
+ """
733
+
734
+ valid_inventory = {
735
+ year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN)
736
+ }
737
+
738
+ complete_inventory = dict(sorted(
739
+ merge(valid_inventory, _calc_missing_equilibrium_years(valid_inventory)).items()
740
+ ))
741
+
742
+ timestamps = [year for year in complete_inventory.keys()]
743
+ land_use_categories = [group[_InventoryKey.LU_CATEGORY] for group in complete_inventory.values()]
744
+ management_categories = [group[_InventoryKey.MG_CATEGORY] for group in complete_inventory.values()]
745
+ carbon_input_categories = [group[_InventoryKey.CI_CATEGORY] for group in complete_inventory.values()]
746
+
747
+ regime_start_years = _calc_regime_start_years(complete_inventory)
748
+
749
+ rng = random.default_rng(seed)
750
+
751
+ soc_ref = _sample_parameter(iterations, ipcc_soil_category, eco_climate_zone, seed=rng)
752
+ land_use_factors = _get_factor_annual(iterations, land_use_categories, eco_climate_zone, seed=rng)
753
+ management_factors = _get_factor_annual(iterations, management_categories, eco_climate_zone, seed=rng)
754
+ carbon_input_factors = _get_factor_annual(iterations, carbon_input_categories, eco_climate_zone, seed=rng)
755
+
756
+ soc_equilibriums = _calc_soc_equilibrium(soc_ref, land_use_factors, management_factors, carbon_input_factors)
757
+ soc_stocks = _calc_soc_stocks(timestamps, regime_start_years, soc_equilibriums)
758
+
759
+ descriptive_stats = calc_descriptive_stats(
760
+ soc_stocks,
761
+ STATS_DEFINITION,
762
+ axis=1, # Calculate stats rowwise.
763
+ decimals=6 # Round values to the nearest milligram.
764
+ )
765
+
766
+ return [_measurement(timestamps, descriptive_stats)]
767
+
768
+
769
+ def _calc_missing_equilibrium_years(inventory: dict) -> dict:
770
+ """
771
+ Calculate any missing inventory years where SOC would have reached equilibrium and return them as a dict.
772
+
773
+ Parameters
774
+ ----------
775
+ inventory : dict
776
+
777
+ Returns
778
+ -------
779
+ dict
780
+ A dictionary of missing equilibrium years with the same structure as `inventory`.
781
+ """
782
+
783
+ min_year, max_year = min(inventory.keys()), max(inventory.keys())
784
+
785
+ def add_missing_equilibrium_year(missing_years: dict, year: int):
786
+ group = inventory[year]
787
+ existing_years = set(list(inventory.keys()) + list(missing_years.keys()))
788
+
789
+ regime_start_year = _calc_regime_start_year(year, inventory)
790
+ equilibrium_year = regime_start_year + _EQUILIBRIUM_TRANSITION_PERIOD
791
+
792
+ should_add_equilibrium = (
793
+ min_year < equilibrium_year < max_year # Is the year relevant?
794
+ and equilibrium_year not in existing_years # Is the year missing?
795
+ and not any(year_ in existing_years for year_ in range(year+1, equilibrium_year)) # Is the year superseded?
796
+ )
797
+
798
+ if should_add_equilibrium:
799
+ missing_years[equilibrium_year] = group
800
+
801
+ return missing_years
802
+
803
+ missing_years = reduce(add_missing_equilibrium_year, inventory.keys(), dict())
804
+
805
+ return missing_years
806
+
807
+
808
+ def _calc_regime_start_years(inventory: dict):
809
+ """
810
+ Calculate when the land-use and land-management regime of all inventory years began.
811
+
812
+ Parameters
813
+ ----------
814
+ inventory : dict
815
+
816
+ Returns
817
+ -------
818
+ list[int]
819
+ """
820
+ return [_calc_regime_start_year(year, inventory) for year in inventory.keys()]
821
+
822
+
823
+ def _calc_regime_start_year(current_year: int, inventory: dict) -> int:
824
+ """
825
+ Calculate when the land-use and land-management regime of a specific inventory year began.
826
+
827
+ Parameters
828
+ ----------
829
+ current_year : int
830
+ inventory : dict
831
+
832
+ Returns
833
+ -------
834
+ int
835
+ """
836
+ MATCH_KEYS = {_InventoryKey.LU_CATEGORY, _InventoryKey.MG_CATEGORY, _InventoryKey.CI_CATEGORY}
837
+ previous_years = list(reversed([year for year in inventory.keys() if year <= current_year]))
838
+ return next(
839
+ (
840
+ previous_years[i-1] for i, previous_year in enumerate(previous_years)
841
+ if not all([
842
+ inventory[current_year][key] == inventory[previous_year][key]
843
+ for key in MATCH_KEYS
844
+ ])
845
+ ),
846
+ previous_years[-1] - _EQUILIBRIUM_TRANSITION_PERIOD
847
+ )
848
+
849
+
850
+ def _get_factor_annual(
851
+ iterations: int,
852
+ category_annual: list[Union[IpccLandUseCategory, IpccManagementCategory, IpccCarbonInputCategory]],
853
+ eco_climate_zone: EcoClimateZone,
854
+ seed: Optional[int] = None
855
+ ) -> NDArray:
856
+ """
857
+ Build an numpy array with the shape `(len(category_annual), iterations)`, where each row represents an inventory
858
+ year and each column contains a sampled value for that year's factor. All rows representing the same factor should
859
+ be identical.
860
+
861
+ Parameters
862
+ ----------
863
+ iterations : int
864
+ The number of samples to take for each year.
865
+ category_annual : list[IpccLandUseCategory | IpccManagementCategory | IpccCarbonInputCategory]
866
+ A list of annual IPCC categories that are linked to SOC stock change factors.
867
+ eco_climate_zone : EcoClimateZone
868
+ The eco-climate zone of the site.
869
+ seed : int | None
870
+ An optional seed for the random sampling of model parameters. If `None`, then fresh, unpredictable entropy will
871
+ be pulled from the OS.
872
+
873
+ Returns
874
+ -------
875
+ NDArray
876
+ The sampled factors as a numpy array.
877
+ """
878
+ param_cache = {
879
+ category: _sample_parameter(iterations, category, eco_climate_zone, seed=seed)
880
+ for category in sorted(set(category_annual), key=lambda category: category.value)
881
+ }
882
+ return vstack([param_cache[category] for category in category_annual])
883
+
884
+
885
+ def _calc_soc_equilibrium(
886
+ soc_ref: NDArray,
887
+ land_use_factor: NDArray,
888
+ management_factor: NDArray,
889
+ carbon_input_factor: NDArray
890
+ ) -> NDArray:
891
+ """
892
+ Calculate the soil organic carbon (SOC) equilibrium based on reference SOC and factors.
893
+
894
+ In the tier 1 model, SOC equilibriums are considered to be reached after 20 years of consistant land use,
895
+ management and carbon input.
896
+
897
+ Parameters
898
+ ----------
899
+ soc_ref : NDArray
900
+ The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
901
+ land_use_factor : NDArray
902
+ The stock change factor for mineral soil organic C land-use systems or sub-systems
903
+ for a particular land-use, dimensionless.
904
+ management_factor : NDArray
905
+ The stock change factor for mineral soil organic C for management regime, dimensionless.
906
+ carbon_input_factor : NDArray
907
+ The stock change factor for mineral soil organic C for the input of organic amendments, dimensionless.
908
+
909
+ Returns
910
+ -------
911
+ NDArray
912
+ The calculated SOC equilibrium, kg C ha-1.
913
+ """
914
+ return soc_ref * land_use_factor * management_factor * carbon_input_factor
915
+
916
+
917
+ def _calc_soc_stocks(
918
+ timestamps: list[int],
919
+ regime_start_years: list[int],
920
+ soc_equilibriums: NDArray
921
+ ) -> NDArray:
922
+ """
923
+ Calculate soil organic carbon (SOC) stocks (kg C ha-1) in the 0-30cm depth interval for each year in the inventory.
924
+
925
+ Parameters
926
+ ----------
927
+ timestamps : list[int]
928
+ A list of timestamps for each year in the inventory.
929
+ regime_start_years : list[int]
930
+ A pre-calculated list of the regime start year for each year in the inventory.
931
+ soc_equilibriums : NDArray
932
+ A numpy array of SOC equilibriums where each row represents a different calendar year.
933
+
934
+ Returns
935
+ -------
936
+ NDArray
937
+ SOC stocks for each year in the inventory.
938
+ """
939
+ soc_stocks = empty_like(soc_equilibriums)
940
+ soc_stocks[0] = soc_equilibriums[0]
941
+
942
+ for index in range(1, len(timestamps)):
943
+
944
+ current_year = timestamps[index]
945
+ current_soc_equilibrium = soc_equilibriums[index]
946
+ current_regime_start_year = regime_start_years[index]
947
+
948
+ previous_index = (
949
+ timestamps.index(current_regime_start_year) - 1 if current_regime_start_year in timestamps else 0
950
+ )
951
+ previous_year = timestamps[previous_index]
952
+ previous_soc_stock = soc_stocks[previous_index]
953
+
954
+ regime_duration = current_year - previous_year
955
+ time_ratio = min(regime_duration / _EQUILIBRIUM_TRANSITION_PERIOD, 1)
956
+ soc_delta = (current_soc_equilibrium - previous_soc_stock) * time_ratio
957
+
958
+ soc_stocks[index] = previous_soc_stock + soc_delta
959
+
960
+ return soc_stocks
961
+
962
+
963
+ # --- COMPILE TIER 1 INVENTORY ---
964
+
965
+
966
+ def _compile_inventory(
967
+ site_type: str, management_nodes: list[dict], measurement_nodes: list[dict]
968
+ ) -> tuple[dict, dict]:
969
+ """
970
+ Builds an annual inventory of data and a dictionary of keyword arguments for the tier 1 model.
971
+
972
+ Parameters
973
+ ----------
974
+ site_id : str
975
+ The `@id` of the site.
976
+ site_type : str
977
+ A valid [site type](https://www-staging.hestia.earth/schema/Site#siteType).
978
+ management_nodes : list[dict]
979
+ A list of [Management nodes](https://www-staging.hestia.earth/schema/Management).
980
+ measurement_nodes: list[dict]
981
+ A list of [Measurement nodes](https://www-staging.hestia.earth/schema/Measurement).
982
+
983
+ Returns
984
+ -------
985
+ tuple[dict, dict]
986
+ A tuple containing `(inventory, kwargs)`.
987
+ """
988
+ eco_climate_zone = _get_eco_climate_zone(measurement_nodes)
989
+ ipcc_soil_category = _assign_ipcc_soil_category(measurement_nodes)
990
+ soc_ref = _get_soc_ref_preview(ipcc_soil_category, eco_climate_zone)
991
+ grouped_management = group_nodes_by_year(management_nodes)
992
+
993
+ # If no `landCover` nodes in `site.management` use `site.siteType` to assign static `IpccLandUseCategory`.
994
+ run_with_site_type = len(filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])) == 0
995
+ site_type_ipcc_land_use_category = SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY.get(site_type, IpccLandUseCategory.OTHER)
996
+
997
+ grouped_management = group_nodes_by_year(management_nodes)
998
+
999
+ grouped_land_use_categories = {
1000
+ year: {
1001
+ _InventoryKey.LU_CATEGORY: (
1002
+ site_type_ipcc_land_use_category if run_with_site_type
1003
+ else _assign_ipcc_land_use_category(nodes, ipcc_soil_category)
1004
+ )
1005
+ } for year, nodes in grouped_management.items()
1006
+ }
1007
+
1008
+ grouped_management_categories = {
1009
+ year: {
1010
+ _InventoryKey.MG_CATEGORY: _assign_ipcc_management_category(
1011
+ nodes,
1012
+ grouped_land_use_categories[year][_InventoryKey.LU_CATEGORY]
1013
+ )
1014
+ } for year, nodes in grouped_management.items()
1015
+ }
1016
+
1017
+ grouped_carbon_input_categories = {
1018
+ year: {
1019
+ _InventoryKey.CI_CATEGORY: _assign_ipcc_carbon_input_category(
1020
+ nodes,
1021
+ grouped_management_categories[year][_InventoryKey.MG_CATEGORY]
1022
+ )
1023
+ } for year, nodes in grouped_management.items()
1024
+ }
1025
+
1026
+ grouped_data = merge(
1027
+ grouped_land_use_categories,
1028
+ grouped_management_categories,
1029
+ grouped_carbon_input_categories
1030
+ )
1031
+
1032
+ grouped_should_run = {
1033
+ year: {_InventoryKey.SHOULD_RUN: _should_run_inventory_year(group)}
1034
+ for year, group in grouped_data.items()
1035
+ }
1036
+
1037
+ inventory = merge(grouped_data, grouped_should_run)
1038
+ kwargs = {
1039
+ "eco_climate_zone": eco_climate_zone,
1040
+ "ipcc_soil_category": ipcc_soil_category,
1041
+ "run_with_site_type": run_with_site_type,
1042
+ "soc_ref": soc_ref
1043
+ }
1044
+
1045
+ return inventory, kwargs
1046
+
1047
+
1048
+ def _get_eco_climate_zone(measurement_nodes: list[dict]) -> Union[EcoClimateZone, None]:
1049
+ """
1050
+ Get the eco-climate zone value from a list of Measurement nodes.
1051
+
1052
+ Parameters
1053
+ ----------
1054
+ measurement_nodes : list[dict]
1055
+ A list of [Measurement nodes](https://www-staging.hestia.earth/schema/Measurement).
1056
+
1057
+ Returns
1058
+ -------
1059
+ EcoClimateZone | None
1060
+ The eco-climate zone value if found, otherwise `None`.
1061
+ """
1062
+ eco_climate_zone = find_term_match(measurement_nodes, "ecoClimateZone")
1063
+ value = get_node_value(eco_climate_zone)
1064
+ return EcoClimateZone(value) if value else None
1065
+
1066
+
1067
+ def _assign_ipcc_soil_category(
1068
+ measurement_nodes: list[dict],
1069
+ default: IpccSoilCategory = IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS
1070
+ ) -> IpccSoilCategory:
1071
+ """
1072
+ Assign an IPCC soil category based on a site's measurement nodes.
1073
+
1074
+ Parameters
1075
+ ----------
1076
+ measurement_nodes : list[dict]
1077
+ List of A list of [Measurement nodes](https://www-staging.hestia.earth/schema/Measurement)..
1078
+ default : IpccSoilCategory, optional
1079
+ The default soil category if none matches, by default IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS.
1080
+
1081
+ Returns
1082
+ -------
1083
+ IpccSoilCategory
1084
+ The assigned IPCC soil category.
1085
+ """
1086
+ soil_types = filter_list_term_type(measurement_nodes, TermTermType.SOILTYPE)
1087
+ usda_soil_types = filter_list_term_type(measurement_nodes, TermTermType.USDASOILTYPE)
1088
+
1089
+ clay_content = get_node_value(find_term_match(measurement_nodes, _CLAY_CONTENT_TERM_ID))
1090
+ sand_content = get_node_value(find_term_match(measurement_nodes, _SAND_CONTENT_TERM_ID))
1091
+
1092
+ has_sandy_soil = clay_content < _CLAY_CONTENT_MAX and sand_content > _SAND_CONTENT_MIN
1093
+
1094
+ return next(
1095
+ (
1096
+ key for key in _SOIL_CATEGORY_DECISION_TREE
1097
+ if _SOIL_CATEGORY_DECISION_TREE[key](
1098
+ key=key,
1099
+ soil_types=soil_types,
1100
+ usda_soil_types=usda_soil_types,
1101
+ has_sandy_soil=has_sandy_soil
1102
+ )
1103
+ ),
1104
+ default
1105
+ ) if len(soil_types) > 0 or len(usda_soil_types) > 0 else default
1106
+
1107
+
1108
+ def _check_soil_category(
1109
+ *,
1110
+ key: IpccSoilCategory,
1111
+ soil_types: list[dict],
1112
+ usda_soil_types: list[dict],
1113
+ **_
1114
+ ) -> bool:
1115
+ """
1116
+ Check if the soil category matches the given key.
1117
+
1118
+ Parameters
1119
+ ----------
1120
+ key : IpccSoilCategory
1121
+ The IPCC soil category to check.
1122
+ soil_types : list[dict]
1123
+ List of soil type measurement nodes.
1124
+ usda_soil_types : list[dict]
1125
+ List of USDA soil type measurement nodes
1126
+
1127
+ Returns
1128
+ -------
1129
+ bool
1130
+ `True` if the soil category matches, `False` otherwise.
1131
+ """
1132
+ SOIL_TYPE_LOOKUP = _LOOKUPS["soilType"]
1133
+ USDA_SOIL_TYPE_LOOKUP = _LOOKUPS["usdaSoilType"]
1134
+
1135
+ target_lookup_values = IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE.get(key, None)
1136
+
1137
+ is_soil_type_match = cumulative_nodes_lookup_match(
1138
+ soil_types,
1139
+ lookup=SOIL_TYPE_LOOKUP,
1140
+ target_lookup_values=target_lookup_values,
1141
+ cumulative_threshold=MIN_AREA_THRESHOLD
1142
+ )
1143
+
1144
+ is_usda_soil_type_match = cumulative_nodes_lookup_match(
1145
+ usda_soil_types,
1146
+ lookup=USDA_SOIL_TYPE_LOOKUP,
1147
+ target_lookup_values=target_lookup_values,
1148
+ cumulative_threshold=MIN_AREA_THRESHOLD
1149
+ )
1150
+
1151
+ return is_soil_type_match or is_usda_soil_type_match
1152
+
1153
+
1154
+ def _check_sandy_soil_category(
1155
+ *,
1156
+ key: IpccSoilCategory,
1157
+ soil_types: list[dict],
1158
+ usda_soil_types: list[dict],
1159
+ has_sandy_soil: bool,
1160
+ **_
1161
+ ) -> bool:
1162
+ """
1163
+ Check if the soils are sandy.
1164
+
1165
+ This function is special case of `_check_soil_category`.
1166
+
1167
+ Parameters
1168
+ ----------
1169
+ key : IpccSoilCategory
1170
+ The IPCC soil category to check.
1171
+ soil_types : list[dict]
1172
+ List of soil type measurement nodes.
1173
+ usda_soil_types : list[dict]
1174
+ List of USDA soil type measurement nodes
1175
+ has_sandy_soil : bool
1176
+ True if the soils are sandy, False otherwise.
1177
+
1178
+ Returns
1179
+ -------
1180
+ bool
1181
+ `True` if the soil category matches, `False` otherwise.
1182
+ """
1183
+ return _check_soil_category(key=key, soil_types=soil_types, usda_soil_types=usda_soil_types) or has_sandy_soil
1184
+
1185
+
1186
+ _SOIL_CATEGORY_DECISION_TREE = {
1187
+ IpccSoilCategory.ORGANIC_SOILS: _check_soil_category,
1188
+ IpccSoilCategory.SANDY_SOILS: _check_sandy_soil_category,
1189
+ IpccSoilCategory.WETLAND_SOILS: _check_soil_category,
1190
+ IpccSoilCategory.VOLCANIC_SOILS: _check_soil_category,
1191
+ IpccSoilCategory.SPODIC_SOILS: _check_soil_category,
1192
+ IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: _check_soil_category,
1193
+ IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: _check_soil_category
1194
+ }
1195
+ """
1196
+ A decision tree mapping IPCC soil categories to corresponding check functions.
1197
+
1198
+ Key: IpccSoilCategory
1199
+ Value: Corresponding function for checking the match of the given soil category based on soil types.
1200
+ """
1201
+
1202
+
1203
+ def _assign_ipcc_land_use_category(
1204
+ management_nodes: list[dict], ipcc_soil_category: IpccSoilCategory,
1205
+ ) -> IpccLandUseCategory:
1206
+ """
1207
+ Assigns IPCC land use category based on management nodes and soil category.
1208
+
1209
+ Parameters
1210
+ ----------
1211
+ management_nodes : list[dict]
1212
+ List of management nodes.
1213
+ ipcc_soil_category : IpccSoilCategory
1214
+ The site"s assigned IPCC soil category.
1215
+
1216
+ Returns
1217
+ -------
1218
+ IpccLandUseCategory
1219
+ Assigned IPCC land use category.
1220
+ """
1221
+ DECISION_TREE = _LAND_USE_CATEGORY_DECISION_TREE
1222
+ DEFAULT = IpccLandUseCategory.OTHER
1223
+
1224
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
1225
+ water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
1226
+
1227
+ has_irrigation = check_irrigation(water_regime_nodes)
1228
+ has_upland_rice = _has_upland_rice(land_cover_nodes)
1229
+ has_irrigated_upland_rice = has_upland_rice and has_irrigation
1230
+ has_long_fallow = _has_long_fallow(land_cover_nodes)
1231
+ has_wetland_soils = ipcc_soil_category is IpccSoilCategory.WETLAND_SOILS
1232
+
1233
+ should_run_ = bool(land_cover_nodes)
1234
+
1235
+ return next(
1236
+ (
1237
+ key for key in DECISION_TREE
1238
+ if DECISION_TREE[key](
1239
+ key=key,
1240
+ land_cover_nodes=land_cover_nodes,
1241
+ has_long_fallow=has_long_fallow,
1242
+ has_irrigated_upland_rice=has_irrigated_upland_rice,
1243
+ has_wetland_soils=has_wetland_soils
1244
+ )
1245
+ ),
1246
+ DEFAULT
1247
+ ) if should_run_ else DEFAULT
1248
+
1249
+
1250
+ def _has_upland_rice(land_cover_nodes: list[dict]) -> bool:
1251
+ """
1252
+ Check if upland rice is present in the land cover nodes.
1253
+
1254
+ Parameters
1255
+ ----------
1256
+ land_cover_nodes : list[dict]
1257
+ List of land cover nodes to be checked.
1258
+
1259
+ Returns
1260
+ -------
1261
+ bool
1262
+ `True` if upland rice is present, `False` otherwise.
1263
+ """
1264
+ return cumulative_nodes_term_match(
1265
+ land_cover_nodes,
1266
+ target_term_ids=get_upland_rice_land_cover_terms_with_cache(),
1267
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
1268
+ )
1269
+
1270
+
1271
+ def _has_long_fallow(land_cover_nodes: list[dict]) -> bool:
1272
+ """
1273
+ Check if long fallow terms are present in the land cover nodes.
1274
+
1275
+ n.b., a super majority of the site area must be under long fallow for it to be classified as set aside.
1276
+
1277
+ Parameters
1278
+ ----------
1279
+ land_cover_nodes : list[dict]
1280
+ List of land cover nodes to be checked.
1281
+
1282
+ Returns
1283
+ -------
1284
+ bool
1285
+ `True` if long fallow is present, `False` otherwise.
1286
+ """
1287
+ LOOKUP = _LOOKUPS["landCover"][0]
1288
+ TARGET_LOOKUP_VALUE = "Set aside"
1289
+ return cumulative_nodes_lookup_match(
1290
+ land_cover_nodes,
1291
+ lookup=LOOKUP,
1292
+ target_lookup_values=TARGET_LOOKUP_VALUE,
1293
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
1294
+ ) or cumulative_nodes_match(
1295
+ lambda node: get_node_property(node, _LONG_FALLOW_CROP_TERM_ID, False).get("value", 0),
1296
+ land_cover_nodes,
1297
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
1298
+ )
1299
+
1300
+
1301
+ def _check_ipcc_land_use_category(*, key: IpccLandUseCategory, land_cover_nodes: list[dict], **kwargs) -> bool:
1302
+ """
1303
+ Check if the land cover nodes and keyword args satisfy the requirements for the given key.
1304
+
1305
+ Parameters
1306
+ ----------
1307
+ key : IpccLandUseCategory
1308
+ The IPCC land use category to check.
1309
+ land_cover_nodes : list[dict]
1310
+ List of land cover nodes to be checked.
1311
+
1312
+ Keyword Args
1313
+ ------------
1314
+ has_irrigated_upland_rice : bool
1315
+ Indicates whether irrigated upland rice is present on more than 30% of the site.
1316
+ has_long_fallow : bool
1317
+ Indicates whether long fallow is present on more than 70% of the site.
1318
+ has_wetland_soils : bool
1319
+ Indicates whether wetland soils are present to more than 30% of the site.
1320
+
1321
+ Returns
1322
+ -------
1323
+ bool
1324
+ `True` if the conditions match the specified land use category, `False` otherwise.
1325
+ """
1326
+ LOOKUP = _LOOKUPS["landCover"][0]
1327
+ target_lookup_values = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(key, None)
1328
+ valid_lookup = cumulative_nodes_lookup_match(
1329
+ land_cover_nodes,
1330
+ lookup=LOOKUP,
1331
+ target_lookup_values=target_lookup_values,
1332
+ cumulative_threshold=MIN_AREA_THRESHOLD
1333
+ )
1334
+
1335
+ validation_kwargs = _IPCC_LAND_USE_CATEGORY_TO_VALIDATION_KWARGS.get(key, set())
1336
+ valid_kwargs = all(v for k, v in kwargs.items() if k in validation_kwargs)
1337
+
1338
+ override_kwargs = _IPCC_LAND_USE_CATEGORY_TO_OVERRIDE_KWARGS.get(key, set())
1339
+ valid_override = any(v for k, v in kwargs.items() if k in override_kwargs)
1340
+
1341
+ return (valid_lookup and valid_kwargs) or valid_override
1342
+
1343
+
1344
+ _IPCC_LAND_USE_CATEGORY_TO_VALIDATION_KWARGS = {
1345
+ IpccLandUseCategory.ANNUAL_CROPS_WET: {"has_wetland_soils"},
1346
+ IpccLandUseCategory.SET_ASIDE: {"has_long_fallow"},
1347
+ }
1348
+ """
1349
+ Keyword arguments that need to be validated in addition to the `landCover` lookup match for specific
1350
+ `IpccLandUseCategory`s.
1351
+ """
1352
+
1353
+ _IPCC_LAND_USE_CATEGORY_TO_OVERRIDE_KWARGS = {
1354
+ IpccLandUseCategory.PADDY_RICE_CULTIVATION: {"has_irrigated_upland_rice"}
1355
+ }
1356
+ """
1357
+ Keyword arguments that can override the `landCover` lookup match for specific `IpccLandUseCategory`s.
1358
+ """
1359
+
1360
+
1361
+ _LAND_USE_CATEGORY_DECISION_TREE = {
1362
+ IpccLandUseCategory.GRASSLAND: _check_ipcc_land_use_category,
1363
+ IpccLandUseCategory.SET_ASIDE: _check_ipcc_land_use_category,
1364
+ IpccLandUseCategory.PERENNIAL_CROPS: _check_ipcc_land_use_category,
1365
+ IpccLandUseCategory.PADDY_RICE_CULTIVATION: _check_ipcc_land_use_category,
1366
+ IpccLandUseCategory.ANNUAL_CROPS_WET: _check_ipcc_land_use_category,
1367
+ IpccLandUseCategory.ANNUAL_CROPS: _check_ipcc_land_use_category,
1368
+ IpccLandUseCategory.FOREST: _check_ipcc_land_use_category,
1369
+ IpccLandUseCategory.NATIVE: _check_ipcc_land_use_category,
1370
+ IpccLandUseCategory.OTHER: _check_ipcc_land_use_category
1371
+ }
1372
+ """
1373
+ A decision tree mapping IPCC soil categories to corresponding check functions.
1374
+
1375
+ Key: IpccLandUseCategory
1376
+ Value: Corresponding function for checking the match of the given land use category based on land cover nodes
1377
+ and additional kwargs.
1378
+ """
1379
+
1380
+
1381
+ def _assign_ipcc_management_category(
1382
+ management_nodes: list[dict], ipcc_land_use_category: IpccLandUseCategory
1383
+ ) -> IpccManagementCategory:
1384
+ """
1385
+ Assign an IPCC Management Category based on the given management nodes and IPCC Land Use Category.
1386
+
1387
+ Parameters
1388
+ ----------
1389
+ management_nodes : list[dict]
1390
+ List of management nodes.
1391
+ ipcc_land_use_category : IpccLandUseCategory
1392
+ The IPCC Land Use Category.
1393
+
1394
+ Returns
1395
+ -------
1396
+ IpccManagementCategory
1397
+ The assigned IPCC Management Category.
1398
+ """
1399
+ decision_tree = _IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE.get(ipcc_land_use_category, {})
1400
+ default = _IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY.get(
1401
+ ipcc_land_use_category, IpccManagementCategory.OTHER
1402
+ )
1403
+
1404
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
1405
+ tillage_nodes = filter_list_term_type(management_nodes, [TermTermType.TILLAGE])
1406
+
1407
+ should_run_ = any([
1408
+ decision_tree == _GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE and len(land_cover_nodes) > 0,
1409
+ decision_tree == _TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE and len(tillage_nodes) > 0
1410
+ ])
1411
+
1412
+ return next(
1413
+ (
1414
+ key for key in decision_tree
1415
+ if decision_tree[key](
1416
+ key=key,
1417
+ land_cover_nodes=land_cover_nodes,
1418
+ tillage_nodes=tillage_nodes,
1419
+ )
1420
+ ),
1421
+ default
1422
+ ) if should_run_ else default
1423
+
1424
+
1425
+ def _check_grassland_ipcc_management_category(
1426
+ *, key: IpccManagementCategory, land_cover_nodes: list[dict], **_
1427
+ ) -> bool:
1428
+ """
1429
+ Check if the land cover nodes match the target conditions for a grassland IpccManagementCategory.
1430
+
1431
+ Parameters
1432
+ ----------
1433
+ key : IpccManagementCategory
1434
+ The IPCC management category to check.
1435
+ land_cover_nodes : list[dict]
1436
+ List of land cover nodes to be checked.
1437
+
1438
+ Returns
1439
+ -------
1440
+ bool
1441
+ `True` if the conditions match the specified management category, `False` otherwise.
1442
+ """
1443
+ target_term_id = IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID.get(key, None)
1444
+ return cumulative_nodes_term_match(
1445
+ land_cover_nodes,
1446
+ target_term_ids=target_term_id,
1447
+ cumulative_threshold=MIN_AREA_THRESHOLD
1448
+ )
1449
+
1450
+
1451
+ _GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE = {
1452
+ IpccManagementCategory.SEVERELY_DEGRADED: _check_grassland_ipcc_management_category,
1453
+ IpccManagementCategory.IMPROVED_GRASSLAND: _check_grassland_ipcc_management_category,
1454
+ IpccManagementCategory.HIGH_INTENSITY_GRAZING: _check_grassland_ipcc_management_category,
1455
+ IpccManagementCategory.NOMINALLY_MANAGED: _check_grassland_ipcc_management_category,
1456
+ IpccManagementCategory.OTHER: _check_grassland_ipcc_management_category
1457
+ }
1458
+ """
1459
+ Decision tree mapping IPCC management categories to corresponding check functions for grassland.
1460
+
1461
+ Key: IpccManagementCategory
1462
+ Value: Corresponding function for checking the match of the given management category based on land cover nodes.
1463
+ """
1464
+
1465
+
1466
+ def _check_tillage_ipcc_management_category(
1467
+ *, key: IpccManagementCategory, tillage_nodes: list[dict], **_
1468
+ ) -> bool:
1469
+ """
1470
+ Check if the tillage nodes match the target conditions for a tillage IpccManagementCategory.
1471
+
1472
+ Parameters
1473
+ ----------
1474
+ key : IpccManagementCategory
1475
+ The IPCC management category to check.
1476
+ tillage_nodes : list[dict]
1477
+ List of tillage nodes to be checked.
1478
+
1479
+ Returns
1480
+ -------
1481
+ bool
1482
+ `True` if the conditions match the specified management category, `False` otherwise.
1483
+ """
1484
+ LOOKUP = _LOOKUPS["tillage"]
1485
+ target_lookup_values = IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE.get(key, None)
1486
+ return cumulative_nodes_lookup_match(
1487
+ tillage_nodes,
1488
+ lookup=LOOKUP,
1489
+ target_lookup_values=target_lookup_values,
1490
+ cumulative_threshold=MIN_AREA_THRESHOLD
1491
+ )
1492
+
1493
+
1494
+ _TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE = {
1495
+ IpccManagementCategory.FULL_TILLAGE: _check_tillage_ipcc_management_category,
1496
+ IpccManagementCategory.REDUCED_TILLAGE: _check_tillage_ipcc_management_category,
1497
+ IpccManagementCategory.NO_TILLAGE: _check_tillage_ipcc_management_category
1498
+ }
1499
+ """
1500
+ Decision tree mapping IPCC management categories to corresponding check functions for tillage.
1501
+
1502
+ Key: IpccManagementCategory
1503
+ Value: Corresponding function for checking the match of the given management category based on tillage nodes.
1504
+ """
1505
+
1506
+
1507
+ _IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE = {
1508
+ IpccLandUseCategory.GRASSLAND: _GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE,
1509
+ IpccLandUseCategory.ANNUAL_CROPS_WET: _TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE,
1510
+ IpccLandUseCategory.ANNUAL_CROPS: _TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE
1511
+ }
1512
+ """
1513
+ Decision tree mapping IPCC land use categories to corresponding decision trees for management categories.
1514
+
1515
+ Key: IpccLandUseCategory
1516
+ Value: Corresponding decision tree for IPCC management categories based on land use categories.
1517
+ """
1518
+
1519
+ _IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY = {
1520
+ IpccLandUseCategory.GRASSLAND: IpccManagementCategory.NOMINALLY_MANAGED,
1521
+ IpccLandUseCategory.ANNUAL_CROPS_WET: IpccManagementCategory.FULL_TILLAGE,
1522
+ IpccLandUseCategory.ANNUAL_CROPS: IpccManagementCategory.FULL_TILLAGE
1523
+ }
1524
+ """
1525
+ Mapping of default IPCC management categories for each IPCC land use category.
1526
+
1527
+ Key: IpccLandUseCategory
1528
+ Value: Default IPCC management category for the given land use category.
1529
+ """
1530
+
1531
+
1532
+ def _assign_ipcc_carbon_input_category(
1533
+ management_nodes: list[dict],
1534
+ ipcc_management_category: IpccManagementCategory
1535
+ ) -> IpccCarbonInputCategory:
1536
+ """
1537
+ Assigns an IPCC Carbon Input Category based on the provided management nodes and IPCC Management Category.
1538
+
1539
+ Parameters
1540
+ ----------
1541
+ management_nodes : list[dict]
1542
+ List of management nodes containing information about land management practices.
1543
+ ipcc_management_category : IpccManagementCategory
1544
+ IPCC Management Category for which the Carbon Input Category needs to be assigned.
1545
+
1546
+ Returns
1547
+ -------
1548
+ IpccCarbonInputCategory
1549
+ Assigned IPCC Carbon Input Category.
1550
+ """
1551
+ decision_tree = _DECISION_TREE_FROM_IPCC_MANAGEMENT_CATEGORY.get(ipcc_management_category, {})
1552
+ default = _DEFAULT_CARBON_INPUT_CATEGORY.get(ipcc_management_category, IpccCarbonInputCategory.OTHER)
1553
+
1554
+ should_run_ = len(management_nodes) > 0
1555
+
1556
+ return next(
1557
+ (key for key in decision_tree if decision_tree[key](
1558
+ key=key,
1559
+ **_get_carbon_input_kwargs(management_nodes)
1560
+ )),
1561
+ default
1562
+ ) if should_run_ else default
1563
+
1564
+
1565
+ def _check_grassland_ipcc_carbon_input_category(
1566
+ *, key: IpccCarbonInputCategory, num_grassland_improvements: int, **_,
1567
+ ) -> bool:
1568
+ """
1569
+ Checks if the given carbon input arguments satisfy the conditions for a specific
1570
+ Grassland IPCC Carbon Input Category.
1571
+
1572
+ Parameters
1573
+ ----------
1574
+ key : IpccCarbonInputCategory
1575
+ The grassland IPCC Carbon Input Category to check.
1576
+ num_grassland_improvements : int
1577
+ The number of grassland improvements.
1578
+
1579
+ Returns
1580
+ -------
1581
+ bool
1582
+ `True` if the conditions for the specified category are met; otherwise, `False`.
1583
+ """
1584
+ return num_grassland_improvements >= _GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS[key]
1585
+
1586
+
1587
+ _GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS = {
1588
+ IpccCarbonInputCategory.GRASSLAND_HIGH: 2,
1589
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM: 1
1590
+ }
1591
+ """
1592
+ A mapping from IPCC Grassland Carbon Input Categories to the minimum number of improvements required.
1593
+
1594
+ Key: IpccCarbonInputCategory
1595
+ Value: Minimum number of improvements required for the corresponding Grassland Carbon Input Category.
1596
+ """
1597
+
1598
+
1599
+ _GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE = {
1600
+ IpccCarbonInputCategory.GRASSLAND_HIGH: _check_grassland_ipcc_carbon_input_category,
1601
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM: _check_grassland_ipcc_carbon_input_category
1602
+ }
1603
+ """
1604
+ A decision tree for assigning IPCC Carbon Input Categories to Grassland based on the number of improvements.
1605
+
1606
+ Key: IpccCarbonInputCategory
1607
+ Value: Corresponding function to check if the given conditions are met for the category.
1608
+ """
1609
+
1610
+
1611
+ def _check_cropland_high_with_manure_category(
1612
+ *,
1613
+ has_animal_manure_used: bool,
1614
+ has_bare_fallow: bool,
1615
+ has_low_residue_producing_crops: bool,
1616
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
1617
+ has_residue_removed_or_burnt: bool,
1618
+ **_
1619
+ ) -> Union[int, None]:
1620
+ """
1621
+ Checks the Cropland High with Manure IPCC Carbon Input Category based on the given carbon input arguments.
1622
+
1623
+ Parameters
1624
+ ----------
1625
+ has_animal_manure_used : bool
1626
+ Indicates whether animal manure is used on more than 30% of the site.
1627
+ has_bare_fallow : bool
1628
+ Indicates whether bare fallow is present on more than 30% of the site.
1629
+ has_low_residue_producing_crops : bool
1630
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
1631
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
1632
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
1633
+ has_residue_removed_or_burnt : bool
1634
+ Indicates whether residues are removed or burnt on more than 30% of the site.
1635
+
1636
+ Returns
1637
+ -------
1638
+ int | none
1639
+ The category key if conditions are met; otherwise, `None`.
1640
+ """
1641
+ conditions = {
1642
+ 1: all([
1643
+ not has_residue_removed_or_burnt,
1644
+ not has_low_residue_producing_crops,
1645
+ not has_bare_fallow,
1646
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
1647
+ has_animal_manure_used
1648
+ ])
1649
+ }
1650
+
1651
+ return next(
1652
+ (key for key, condition in conditions.items() if condition), None
1653
+ )
1654
+
1655
+
1656
+ def _check_cropland_high_without_manure_category(
1657
+ *,
1658
+ has_animal_manure_used: bool,
1659
+ has_bare_fallow: bool,
1660
+ has_cover_crop: bool,
1661
+ has_irrigation: bool,
1662
+ has_low_residue_producing_crops: bool,
1663
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
1664
+ has_organic_fertiliser_or_soil_amendment_used: bool,
1665
+ has_practice_increasing_c_input: bool,
1666
+ has_residue_removed_or_burnt: bool,
1667
+ **_
1668
+ ) -> Union[int, None]:
1669
+ """
1670
+ Checks the Cropland High without Manure IPCC Carbon Input Category based on the given carbon input arguments.
1671
+
1672
+ Parameters
1673
+ ----------
1674
+ has_animal_manure_used : bool
1675
+ Indicates whether animal manure is used on more than 30% of the site.
1676
+ has_bare_fallow : bool
1677
+ Indicates whether bare fallow is present on more than 30% of the site.
1678
+ has_cover_crop : bool
1679
+ Indicates whether cover crops are present on more than 30% of the site.
1680
+ has_irrigation : bool
1681
+ Indicates whether irrigation is applied to more than 30% of the site.
1682
+ has_low_residue_producing_crops : bool
1683
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
1684
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
1685
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
1686
+ has_organic_fertiliser_or_soil_amendment_used : bool
1687
+ Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
1688
+ has_practice_increasing_c_input : bool
1689
+ Indicates whether practices increasing carbon input are present on more than 30% of the site.
1690
+ has_residue_removed_or_burnt : bool
1691
+ Indicates whether residues are removed or burnt on more than 30% of the site.
1692
+
1693
+ Returns
1694
+ -------
1695
+ int | None
1696
+ The category key if conditions are met; otherwise, `None`.
1697
+ """
1698
+ conditions = {
1699
+ 1: all([
1700
+ not has_residue_removed_or_burnt,
1701
+ not has_low_residue_producing_crops,
1702
+ not has_bare_fallow,
1703
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
1704
+ any([
1705
+ has_irrigation,
1706
+ has_practice_increasing_c_input,
1707
+ has_cover_crop,
1708
+ has_organic_fertiliser_or_soil_amendment_used
1709
+ ]),
1710
+ not has_animal_manure_used
1711
+ ])
1712
+ }
1713
+
1714
+ return next(
1715
+ (key for key, condition in conditions.items() if condition), None
1716
+ )
1717
+
1718
+
1719
+ def _check_cropland_medium_category(
1720
+ *,
1721
+ has_animal_manure_used: bool,
1722
+ has_bare_fallow: bool,
1723
+ has_cover_crop: bool,
1724
+ has_irrigation: bool,
1725
+ has_low_residue_producing_crops: bool,
1726
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
1727
+ has_organic_fertiliser_or_soil_amendment_used: bool,
1728
+ has_practice_increasing_c_input: bool,
1729
+ has_residue_removed_or_burnt: bool,
1730
+ **_
1731
+ ) -> Union[int, None]:
1732
+ """
1733
+ Checks the Cropland Medium IPCC Carbon Input Category based on the given carbon input arguments.
1734
+
1735
+ Parameters
1736
+ ----------
1737
+ has_animal_manure_used : bool
1738
+ Indicates whether animal manure is used on more than 30% of the site.
1739
+ has_bare_fallow : bool
1740
+ Indicates whether bare fallow is present on more than 30% of the site.
1741
+ has_cover_crop : bool
1742
+ Indicates whether cover crops are present on more than 30% of the site.
1743
+ has_irrigation : bool
1744
+ Indicates whether irrigation is applied to more than 30% of the site.
1745
+ has_low_residue_producing_crops : bool
1746
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
1747
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
1748
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
1749
+ has_organic_fertiliser_or_soil_amendment_used : bool
1750
+ Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
1751
+ has_practice_increasing_c_input : bool
1752
+ Indicates whether practices increasing carbon input are present on more than 30% of the site.
1753
+ has_residue_removed_or_burnt : bool
1754
+ Indicates whether residues are removed or burnt on more than 30% of the site.
1755
+
1756
+ Returns
1757
+ -------
1758
+ int | None
1759
+ The category key if conditions are met; otherwise, `None`.
1760
+ """
1761
+ conditions = {
1762
+ 1: all([
1763
+ has_residue_removed_or_burnt,
1764
+ has_animal_manure_used
1765
+ ]),
1766
+ 2: all([
1767
+ not has_residue_removed_or_burnt,
1768
+ any([
1769
+ has_low_residue_producing_crops,
1770
+ has_bare_fallow
1771
+ ]),
1772
+ any([
1773
+ has_irrigation,
1774
+ has_practice_increasing_c_input,
1775
+ has_cover_crop,
1776
+ has_organic_fertiliser_or_soil_amendment_used,
1777
+ ])
1778
+ ]),
1779
+ 3: all([
1780
+ not has_residue_removed_or_burnt,
1781
+ not has_low_residue_producing_crops,
1782
+ not has_bare_fallow,
1783
+ not has_n_fixing_crop_or_inorganic_n_fertiliser_used,
1784
+ any([
1785
+ has_irrigation,
1786
+ has_practice_increasing_c_input,
1787
+ has_cover_crop,
1788
+ has_organic_fertiliser_or_soil_amendment_used
1789
+ ])
1790
+ ]),
1791
+ 4: all([
1792
+ not has_residue_removed_or_burnt,
1793
+ not has_low_residue_producing_crops,
1794
+ not has_bare_fallow,
1795
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
1796
+ not has_irrigation,
1797
+ not has_organic_fertiliser_or_soil_amendment_used,
1798
+ not has_practice_increasing_c_input,
1799
+ not has_cover_crop
1800
+ ])
1801
+ }
1802
+
1803
+ return next(
1804
+ (key for key, condition in conditions.items() if condition), None
1805
+ )
1806
+
1807
+
1808
+ def _check_cropland_low_category(
1809
+ *,
1810
+ has_animal_manure_used: bool,
1811
+ has_bare_fallow: bool,
1812
+ has_cover_crop: bool,
1813
+ has_irrigation: bool,
1814
+ has_low_residue_producing_crops: bool,
1815
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
1816
+ has_organic_fertiliser_or_soil_amendment_used: bool,
1817
+ has_practice_increasing_c_input: bool,
1818
+ has_residue_removed_or_burnt: bool,
1819
+ **_
1820
+ ) -> Union[int, None]:
1821
+ """
1822
+ Checks the Cropland Low IPCC Carbon Input Category based on the given carbon input arguments.
1823
+
1824
+ Parameters
1825
+ ----------
1826
+ has_animal_manure_used : bool
1827
+ Indicates whether animal manure is used on more than 30% of the site.
1828
+ has_bare_fallow : bool
1829
+ Indicates whether bare fallow is present on more than 30% of the site.
1830
+ has_cover_crop : bool
1831
+ Indicates whether cover crops are present on more than 30% of the site.
1832
+ has_irrigation : bool
1833
+ Indicates whether irrigation is applied to more than 30% of the site.
1834
+ has_low_residue_producing_crops : bool
1835
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
1836
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
1837
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
1838
+ has_organic_fertiliser_or_soil_amendment_used : bool
1839
+ Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
1840
+ has_practice_increasing_c_input : bool
1841
+ Indicates whether practices increasing carbon input are present on more than 30% of the site.
1842
+ has_residue_removed_or_burnt : bool
1843
+ Indicates whether residues are removed or burnt on more than 30% of the site.
1844
+
1845
+ Returns
1846
+ -------
1847
+ int | None
1848
+ The category key if conditions are met; otherwise, `None`.
1849
+ """
1850
+ conditions = {
1851
+ 1: all([
1852
+ has_residue_removed_or_burnt,
1853
+ not has_animal_manure_used
1854
+ ]),
1855
+ 2: all([
1856
+ not has_residue_removed_or_burnt,
1857
+ any([
1858
+ has_low_residue_producing_crops,
1859
+ has_bare_fallow
1860
+ ]),
1861
+ not has_irrigation,
1862
+ not has_practice_increasing_c_input,
1863
+ not has_cover_crop,
1864
+ not has_organic_fertiliser_or_soil_amendment_used
1865
+ ]),
1866
+ 3: all([
1867
+ not has_residue_removed_or_burnt,
1868
+ not has_low_residue_producing_crops,
1869
+ not has_bare_fallow,
1870
+ not has_n_fixing_crop_or_inorganic_n_fertiliser_used,
1871
+ not has_irrigation,
1872
+ not has_organic_fertiliser_or_soil_amendment_used,
1873
+ not has_practice_increasing_c_input,
1874
+ not has_cover_crop
1875
+ ])
1876
+ }
1877
+
1878
+ return next(
1879
+ (key for key, condition in conditions.items() if condition), None
1880
+ )
1881
+
1882
+
1883
+ _CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE = {
1884
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE: _check_cropland_high_with_manure_category,
1885
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE: _check_cropland_high_without_manure_category,
1886
+ IpccCarbonInputCategory.CROPLAND_MEDIUM: _check_cropland_medium_category,
1887
+ IpccCarbonInputCategory.CROPLAND_LOW: _check_cropland_low_category
1888
+ }
1889
+ """
1890
+ A decision tree for assigning IPCC Carbon Input Categories to Cropland based on specific conditions.
1891
+
1892
+ Key: IpccCarbonInputCategory
1893
+ Value: Corresponding function to check if the given conditions are met for the category.
1894
+ """
1895
+
1896
+ _DECISION_TREE_FROM_IPCC_MANAGEMENT_CATEGORY = {
1897
+ IpccManagementCategory.IMPROVED_GRASSLAND: _GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE,
1898
+ IpccManagementCategory.FULL_TILLAGE: _CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE,
1899
+ IpccManagementCategory.REDUCED_TILLAGE: _CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE,
1900
+ IpccManagementCategory.NO_TILLAGE: _CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE
1901
+ }
1902
+ """
1903
+ A decision tree mapping IPCC Management Categories to respective Carbon Input Category decision trees.
1904
+
1905
+ Key: IpccManagementCategory
1906
+ Value: Decision tree for Carbon Input Categories corresponding to the management category.
1907
+ """
1908
+
1909
+ _DEFAULT_CARBON_INPUT_CATEGORY = {
1910
+ IpccManagementCategory.IMPROVED_GRASSLAND: IpccCarbonInputCategory.GRASSLAND_MEDIUM,
1911
+ IpccManagementCategory.FULL_TILLAGE: IpccCarbonInputCategory.CROPLAND_LOW,
1912
+ IpccManagementCategory.REDUCED_TILLAGE: IpccCarbonInputCategory.CROPLAND_LOW,
1913
+ IpccManagementCategory.NO_TILLAGE: IpccCarbonInputCategory.CROPLAND_LOW
1914
+ }
1915
+ """
1916
+ A mapping from IPCC Management Categories to default Carbon Input Categories.
1917
+
1918
+ Key: IpccManagementCategory
1919
+ Value: Default Carbon Input Category for the corresponding Management Category.
1920
+ """
1921
+
1922
+
1923
+ def _get_carbon_input_kwargs(
1924
+ management_nodes: list[dict]
1925
+ ) -> dict:
1926
+ """
1927
+ Creates CarbonInputArgs based on the provided list of management nodes.
1928
+
1929
+ Parameters
1930
+ ----------
1931
+ management_nodes : list[dict]
1932
+ The list of management nodes.
1933
+
1934
+ Returns
1935
+ -------
1936
+ dict
1937
+ The carbon input keyword arguments.
1938
+ """
1939
+
1940
+ PRACTICE_INCREASING_C_INPUT_LOOKUP = _LOOKUPS["landUseManagement"]
1941
+ LOW_RESIDUE_PRODUCING_CROP_LOOKUP = _LOOKUPS["landCover"][1]
1942
+ N_FIXING_CROP_LOOKUP = _LOOKUPS["landCover"][2]
1943
+
1944
+ # To prevent double counting already explicitly checked practices.
1945
+ EXCLUDED_PRACTICE_TERM_IDS = {
1946
+ _IMPROVED_PASTURE_TERM_ID,
1947
+ _ANIMAL_MANURE_USED_TERM_ID,
1948
+ _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID,
1949
+ _ORGANIC_FERTILISER_USED_TERM_ID
1950
+ }
1951
+
1952
+ crop_residue_management_nodes = filter_list_term_type(management_nodes, [TermTermType.CROPRESIDUEMANAGEMENT])
1953
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
1954
+ land_use_management_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDUSEMANAGEMENT])
1955
+ water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
1956
+
1957
+ has_animal_manure_used = any(
1958
+ get_node_value(node) for node in land_use_management_nodes if node_term_match(node, _ANIMAL_MANURE_USED_TERM_ID)
1959
+ )
1960
+
1961
+ has_bare_fallow = cumulative_nodes_term_match(
1962
+ land_cover_nodes,
1963
+ target_term_ids=_SHORT_BARE_FALLOW_TERM_ID,
1964
+ cumulative_threshold=MIN_AREA_THRESHOLD
1965
+ )
1966
+
1967
+ has_cover_crop = cumulative_nodes_match(
1968
+ lambda node: any(
1969
+ get_node_property(node, term_id, False).get("value", False)
1970
+ for term_id in get_cover_crop_property_terms_with_cache()
1971
+ ),
1972
+ land_cover_nodes,
1973
+ cumulative_threshold=MIN_AREA_THRESHOLD
1974
+ )
1975
+
1976
+ has_inorganic_n_fertiliser_used = any(
1977
+ get_node_value(node) for node in land_use_management_nodes
1978
+ if node_term_match(node, _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID)
1979
+ )
1980
+
1981
+ has_irrigation = check_irrigation(water_regime_nodes)
1982
+
1983
+ has_low_residue_producing_crops = cumulative_nodes_lookup_match(
1984
+ land_cover_nodes,
1985
+ lookup=LOW_RESIDUE_PRODUCING_CROP_LOOKUP,
1986
+ target_lookup_values=True,
1987
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD # Requires a supermajority (>70%).
1988
+ )
1989
+
1990
+ has_n_fixing_crop = cumulative_nodes_lookup_match(
1991
+ land_cover_nodes,
1992
+ lookup=N_FIXING_CROP_LOOKUP,
1993
+ target_lookup_values=True,
1994
+ cumulative_threshold=MIN_AREA_THRESHOLD
1995
+ )
1996
+
1997
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used = has_n_fixing_crop or has_inorganic_n_fertiliser_used
1998
+
1999
+ has_organic_fertiliser_or_soil_amendment_used = any(
2000
+ get_node_value(node) for node in land_use_management_nodes
2001
+ if node_term_match(node, [_ORGANIC_FERTILISER_USED_TERM_ID, _SOIL_AMENDMENT_USED_TERM_ID])
2002
+ )
2003
+
2004
+ has_practice_increasing_c_input = cumulative_nodes_match(
2005
+ lambda node: (
2006
+ node_lookup_match(node, PRACTICE_INCREASING_C_INPUT_LOOKUP, True)
2007
+ and not node_term_match(node, EXCLUDED_PRACTICE_TERM_IDS)
2008
+ ),
2009
+ land_use_management_nodes,
2010
+ cumulative_threshold=MIN_AREA_THRESHOLD
2011
+ )
2012
+
2013
+ has_residue_removed_or_burnt = cumulative_nodes_term_match(
2014
+ crop_residue_management_nodes,
2015
+ target_term_ids=get_residue_removed_or_burnt_terms_with_cache(),
2016
+ cumulative_threshold=MIN_AREA_THRESHOLD
2017
+ )
2018
+
2019
+ num_grassland_improvements = [
2020
+ has_irrigation,
2021
+ has_practice_increasing_c_input,
2022
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
2023
+ has_organic_fertiliser_or_soil_amendment_used
2024
+ ].count(True)
2025
+
2026
+ return {
2027
+ "has_animal_manure_used": has_animal_manure_used,
2028
+ "has_bare_fallow": has_bare_fallow,
2029
+ "has_cover_crop": has_cover_crop,
2030
+ "has_irrigation": has_irrigation,
2031
+ "has_low_residue_producing_crops": has_low_residue_producing_crops,
2032
+ "has_n_fixing_crop_or_inorganic_n_fertiliser_used": has_n_fixing_crop_or_inorganic_n_fertiliser_used,
2033
+ "has_organic_fertiliser_or_soil_amendment_used": has_organic_fertiliser_or_soil_amendment_used,
2034
+ "has_practice_increasing_c_input": has_practice_increasing_c_input,
2035
+ "has_residue_removed_or_burnt": has_residue_removed_or_burnt,
2036
+ "num_grassland_improvements": num_grassland_improvements
2037
+ }
2038
+
2039
+
2040
+ def _should_run_inventory_year(group: dict) -> bool:
2041
+ """
2042
+ Determines whether there is sufficient data in an inventory year to run the tier 1 model.
2043
+
2044
+ 1. Check if the land use category is not "OTHER"
2045
+ 2. Check if all required keys are present.
2046
+
2047
+ Parameters
2048
+ ----------
2049
+ group : dict
2050
+ Dictionary containing information for a specific inventory year.
2051
+
2052
+ Returns
2053
+ -------
2054
+ bool
2055
+ True if the inventory year is valid, False otherwise.
2056
+ """
2057
+ return all([
2058
+ group.get(_InventoryKey.LU_CATEGORY) != IpccLandUseCategory.OTHER,
2059
+ all(key in group.keys() for key in _REQUIRED_KEYS),
2060
+ ])