hestia-earth-models 0.61.6__py3-none-any.whl → 0.61.8__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 (52) hide show
  1. hestia_earth/models/cycle/completeness/electricityFuel.py +56 -0
  2. hestia_earth/models/cycle/input/hestiaAggregatedData.py +1 -1
  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/belowGroundCropResidue.py +1 -1
  9. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -1
  10. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +511 -458
  11. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +5 -1
  12. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +117 -3881
  13. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2060 -0
  14. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1630 -0
  15. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +324 -0
  16. hestia_earth/models/mocking/search-results.json +360 -260
  17. hestia_earth/models/schererPfister2015/pToDrainageWaterSoilFlux.py +1 -1
  18. hestia_earth/models/schererPfister2015/pToGroundwaterSoilFlux.py +1 -1
  19. hestia_earth/models/site/organicCarbonPerHa.py +58 -44
  20. hestia_earth/models/site/soilMeasurement.py +25 -38
  21. hestia_earth/models/utils/__init__.py +28 -0
  22. hestia_earth/models/utils/aquacultureManagement.py +2 -2
  23. hestia_earth/models/utils/array_builders.py +578 -0
  24. hestia_earth/models/utils/blank_node.py +2 -3
  25. hestia_earth/models/utils/crop.py +24 -1
  26. hestia_earth/models/utils/cycle.py +0 -23
  27. hestia_earth/models/utils/descriptive_stats.py +285 -0
  28. hestia_earth/models/utils/emission.py +73 -2
  29. hestia_earth/models/utils/inorganicFertiliser.py +2 -2
  30. hestia_earth/models/utils/lookup.py +6 -3
  31. hestia_earth/models/utils/measurement.py +118 -4
  32. hestia_earth/models/utils/site.py +25 -13
  33. hestia_earth/models/version.py +1 -1
  34. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/METADATA +1 -1
  35. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/RECORD +52 -40
  36. tests/models/cycle/completeness/test_electricityFuel.py +21 -0
  37. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +2 -2
  38. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +54 -165
  39. tests/models/ipcc2019/test_organicCarbonPerHa.py +219 -460
  40. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +471 -0
  41. tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +208 -0
  42. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +75 -0
  43. tests/models/site/test_organicCarbonPerHa.py +3 -12
  44. tests/models/site/test_soilMeasurement.py +5 -19
  45. tests/models/utils/test_array_builders.py +253 -0
  46. tests/models/utils/{test_cycle.py → test_crop.py} +2 -2
  47. tests/models/utils/test_descriptive_stats.py +134 -0
  48. tests/models/utils/test_emission.py +51 -1
  49. tests/models/utils/test_measurement.py +54 -2
  50. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/LICENSE +0 -0
  51. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/WHEEL +0 -0
  52. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,471 @@
1
+ from itertools import product
2
+ import json
3
+ from numpy import array
4
+ from numpy.testing import assert_array_almost_equal
5
+ from pytest import mark
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ from hestia_earth.models.ipcc2019.organicCarbonPerHa import MODEL, TERM_ID
9
+ from hestia_earth.models.ipcc2019.organicCarbonPerHa_utils import (
10
+ EcoClimateZone, IpccCarbonInputCategory, IpccLandUseCategory, IpccManagementCategory,
11
+ IpccSoilCategory, sample_constant, sample_plus_minus_error, sample_plus_minus_uncertainty
12
+ )
13
+ from hestia_earth.models.ipcc2019.organicCarbonPerHa_tier_1_utils import (
14
+ _assign_ipcc_carbon_input_category, _assign_ipcc_land_use_category, _assign_ipcc_management_category,
15
+ _assign_ipcc_soil_category, _calc_missing_equilibrium_years, _calc_regime_start_years, _calc_soc_stocks,
16
+ _check_cropland_low_category, _check_cropland_medium_category, _get_carbon_input_kwargs, _get_sample_func,
17
+ _get_soc_ref_preview, _InventoryKey, _sample_parameter, _EXCLUDED_ECO_CLIMATE_ZONES
18
+ )
19
+
20
+ from tests.utils import fixtures_path
21
+
22
+ class_path = f"hestia_earth.models.{MODEL}.{TERM_ID}_tier_1_utils"
23
+ utils_path = f"hestia_earth.models.{MODEL}.{TERM_ID}_utils"
24
+ term_path = "hestia_earth.models.utils.term"
25
+ property_path = "hestia_earth.models.utils.property"
26
+
27
+ fixtures_folder = f"{fixtures_path}/{MODEL}/{TERM_ID}"
28
+
29
+ ITERATIONS = 1000
30
+
31
+ COVER_CROP_PROPERTY_TERM_IDS = [
32
+ "catchCrop",
33
+ "coverCrop",
34
+ "groundCover",
35
+ "longFallowCrop",
36
+ "shortFallowCrop"
37
+ ]
38
+
39
+ IRRIGATED_TERM_IDS = [
40
+ "rainfedDeepWater",
41
+ "rainfedDeepWaterWaterDepth100Cm",
42
+ "rainfedDeepWaterWaterDepth50100Cm",
43
+ "irrigatedTypeUnspecified",
44
+ "irrigatedCenterPivotIrrigation",
45
+ "irrigatedContinuouslyFlooded",
46
+ "irrigatedDripIrrigation",
47
+ "irrigatedFurrowIrrigation",
48
+ "irrigatedLateralMoveIrrigation",
49
+ "irrigatedLocalizedIrrigation",
50
+ "irrigatedManualIrrigation",
51
+ "irrigatedSurfaceIrrigationMultipleDrainagePeriods",
52
+ "irrigatedSurfaceIrrigationSingleDrainagePeriod",
53
+ "irrigatedSprinklerIrrigation",
54
+ "irrigatedSubIrrigation",
55
+ "irrigatedSurfaceIrrigationDrainageRegimeUnspecified"
56
+ ]
57
+
58
+ RESIDUE_REMOVED_OR_BURNT_TERM_IDS = [
59
+ "residueBurnt",
60
+ "residueRemoved"
61
+ ]
62
+
63
+ UPLAND_RICE_LAND_COVER_TERM_IDS = [
64
+ "ricePlantUpland"
65
+ ]
66
+
67
+
68
+ # kwargs, sample_func, expected_shape
69
+ PARAMS_GET_SAMPLE_FUNC = [
70
+ ({"value": 1}, sample_constant),
71
+ ({"value": 1, "error": 10}, sample_plus_minus_error),
72
+ ({"value": 1, "uncertainty": 10}, sample_plus_minus_uncertainty)
73
+ ]
74
+ IDS_GET_SAMPLE_FUNC = ["constant", "+/- error", "+/- uncertainty"]
75
+
76
+
77
+ @mark.parametrize("kwargs, sample_func", PARAMS_GET_SAMPLE_FUNC, ids=IDS_GET_SAMPLE_FUNC)
78
+ def test_get_sample_func(kwargs, sample_func):
79
+ result = _get_sample_func(kwargs)
80
+ assert result == sample_func
81
+
82
+
83
+ SOC_REF_PARAMS = [p for p in product(IpccSoilCategory, EcoClimateZone) if _get_soc_ref_preview(*p)]
84
+ FACTOR_PARAMS = list(product(
85
+ [c for c in IpccLandUseCategory] + [c for c in IpccManagementCategory] + [c for c in IpccCarbonInputCategory],
86
+ [e for e in EcoClimateZone if e not in _EXCLUDED_ECO_CLIMATE_ZONES]
87
+ ))
88
+
89
+ # ipcc_category, eco_climate_zone
90
+ PARAMS_SAMPLE_PARAMETER = SOC_REF_PARAMS + FACTOR_PARAMS
91
+ IDS_SAMPLE_PARAMETER = [f"{p[0]} + {p[1].name}" for p in PARAMS_SAMPLE_PARAMETER]
92
+
93
+
94
+ @mark.parametrize("ipcc_category, eco_climate_zone", PARAMS_SAMPLE_PARAMETER, ids=IDS_SAMPLE_PARAMETER)
95
+ def test_sample_parameter(ipcc_category, eco_climate_zone):
96
+ """
97
+ Check that every combination of parameter and eco_climate_zone can be sampled without raising an error.
98
+ """
99
+ EXPECTED_SHAPE = (1, ITERATIONS)
100
+ result = _sample_parameter(ITERATIONS, ipcc_category, eco_climate_zone)
101
+ assert result.shape == EXPECTED_SHAPE
102
+
103
+
104
+ # subfolder, expected
105
+ SOIL_CATEGORY_PARAMS = [
106
+ ("fractional", IpccSoilCategory.WETLAND_SOILS),
107
+ ("no-measurements", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS),
108
+ ("sandy-override", IpccSoilCategory.SANDY_SOILS),
109
+ ("soilType/hac", IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS),
110
+ ("soilType/lac", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS),
111
+ ("soilType/org", IpccSoilCategory.ORGANIC_SOILS),
112
+ ("soilType/pod", IpccSoilCategory.SPODIC_SOILS),
113
+ ("soilType/san", IpccSoilCategory.SANDY_SOILS),
114
+ ("soilType/vol", IpccSoilCategory.VOLCANIC_SOILS),
115
+ ("soilType/wet", IpccSoilCategory.WETLAND_SOILS),
116
+ ("usdaSoilType/hac", IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS),
117
+ ("usdaSoilType/lac", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS),
118
+ ("usdaSoilType/org", IpccSoilCategory.ORGANIC_SOILS),
119
+ ("usdaSoilType/pod", IpccSoilCategory.SPODIC_SOILS),
120
+ ("usdaSoilType/san", IpccSoilCategory.SANDY_SOILS),
121
+ ("usdaSoilType/vol", IpccSoilCategory.VOLCANIC_SOILS),
122
+ ("usdaSoilType/wet", IpccSoilCategory.WETLAND_SOILS)
123
+ ]
124
+
125
+
126
+ @mark.parametrize(
127
+ "subfolder, expected",
128
+ SOIL_CATEGORY_PARAMS,
129
+ ids=[params[0] for params in SOIL_CATEGORY_PARAMS]
130
+ )
131
+ def test_assign_ipcc_soil_category(subfolder: str, expected: IpccSoilCategory):
132
+ folder = f"{fixtures_folder}/IpccSoilCategory/{subfolder}"
133
+
134
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
135
+ site = json.load(f)
136
+
137
+ result = _assign_ipcc_soil_category(site.get("measurements", []))
138
+ assert result == expected
139
+
140
+
141
+ # subfolder, soil_category, expected
142
+ LAND_USE_CATEGORY_PARAMS = [
143
+ ("annual-crops", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.ANNUAL_CROPS),
144
+ ("annual-crops-wet", IpccSoilCategory.WETLAND_SOILS, IpccLandUseCategory.ANNUAL_CROPS_WET),
145
+ ("forest", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.FOREST),
146
+ ("fractional", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PERENNIAL_CROPS),
147
+ ("grassland", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.GRASSLAND),
148
+ ("irrigated-upland-rice", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PADDY_RICE_CULTIVATION),
149
+ ("native", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.NATIVE),
150
+ ("other", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.OTHER),
151
+ ("paddy-rice-cultivation", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PADDY_RICE_CULTIVATION),
152
+ ("perennial-crops", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PERENNIAL_CROPS),
153
+ ("set-aside", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.SET_ASIDE),
154
+ ("set-aside-override", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.SET_ASIDE),
155
+ ("upland-rice", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.ANNUAL_CROPS),
156
+ ]
157
+
158
+
159
+ @mark.parametrize(
160
+ "subfolder, soil_category, expected",
161
+ LAND_USE_CATEGORY_PARAMS,
162
+ ids=[params[0] for params in LAND_USE_CATEGORY_PARAMS]
163
+ )
164
+ @patch(f"{term_path}.search")
165
+ @patch(f"{utils_path}.get_upland_rice_land_cover_terms", return_value=UPLAND_RICE_LAND_COVER_TERM_IDS)
166
+ @patch(f"{utils_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
167
+ def test_assign_ipcc_land_use_category(
168
+ mock_get_irrigated_terms: MagicMock,
169
+ mock_get_upland_rice_land_cover_terms: MagicMock,
170
+ mock_search: MagicMock,
171
+ subfolder: str,
172
+ soil_category: IpccSoilCategory,
173
+ expected: IpccLandUseCategory
174
+ ):
175
+ folder = f"{fixtures_folder}/IpccLandUseCategory/{subfolder}"
176
+
177
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
178
+ site = json.load(f)
179
+
180
+ result = _assign_ipcc_land_use_category(site.get("management", []), soil_category)
181
+
182
+ # Ensure that API calls to retrieve term IDs are properly cached.
183
+ mock_get_irrigated_terms.call_count <= 1
184
+ mock_get_upland_rice_land_cover_terms.call_count <= 1
185
+
186
+ mock_search.assert_not_called() # Ensure that the term utils are properly mocked.
187
+ assert result == expected
188
+
189
+
190
+ # subfolder, land_use_category, expected
191
+ MANAGEMENT_CATEGORY_PARAMS = [
192
+ ("fractional-annual-crops", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.REDUCED_TILLAGE),
193
+ ("fractional-annual-crops-wet", IpccLandUseCategory.ANNUAL_CROPS_WET, IpccManagementCategory.REDUCED_TILLAGE),
194
+ ("fractional-grassland", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.IMPROVED_GRASSLAND),
195
+ ("full-tillage", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.FULL_TILLAGE),
196
+ ("high-intensity-grazing", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.HIGH_INTENSITY_GRAZING),
197
+ ("improved-grassland", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.IMPROVED_GRASSLAND),
198
+ ("no-management/annual-crops", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.FULL_TILLAGE),
199
+ ("no-management/annual-crops-wet", IpccLandUseCategory.ANNUAL_CROPS_WET, IpccManagementCategory.FULL_TILLAGE),
200
+ ("no-management/grassland", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.NOMINALLY_MANAGED),
201
+ ("no-tillage", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.NO_TILLAGE),
202
+ ("nominally-managed", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.NOMINALLY_MANAGED),
203
+ ("other", IpccLandUseCategory.OTHER, IpccManagementCategory.OTHER),
204
+ ("reduced-tillage", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.REDUCED_TILLAGE),
205
+ ("severely-degraded", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.SEVERELY_DEGRADED),
206
+ ]
207
+
208
+
209
+ @mark.parametrize(
210
+ "subfolder, land_use_category, expected",
211
+ MANAGEMENT_CATEGORY_PARAMS,
212
+ ids=[params[0] for params in MANAGEMENT_CATEGORY_PARAMS]
213
+ )
214
+ def test_assign_ipcc_management_category(
215
+ subfolder: str, land_use_category: IpccLandUseCategory, expected: IpccManagementCategory
216
+ ):
217
+ folder = f"{fixtures_folder}/IpccManagementCategory/{subfolder}"
218
+
219
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
220
+ site = json.load(f)
221
+
222
+ result = _assign_ipcc_management_category(site.get("management", []), land_use_category)
223
+ assert result == expected
224
+
225
+
226
+ @mark.parametrize("key", [1, 2, 3, 4], ids=lambda key: f"scenario-{key}")
227
+ @patch(f"{term_path}.search")
228
+ @patch(f"{utils_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
229
+ @patch(f"{utils_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
230
+ @patch(f"{utils_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
231
+ def test_check_cropland_medium_category(
232
+ mock_get_cover_crop_property_terms: MagicMock,
233
+ mock_get_irrigated_terms: MagicMock,
234
+ mock_get_residue_removed_or_burnt_terms: MagicMock,
235
+ mock_search: MagicMock,
236
+ key: int
237
+ ):
238
+ """
239
+ Tests each set of cropland medium conditions against a list of nodes that such satisfy it. The function returns the
240
+ key of the matching condition set, which should match the suffix of the fixtures subfolder.
241
+ """
242
+ folder = f"{fixtures_folder}/IpccCarbonInputCategory/cropland-medium/scenario-{key}"
243
+
244
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
245
+ site = json.load(f)
246
+
247
+ result = _check_cropland_medium_category(**_get_carbon_input_kwargs(site.get("management", [])))
248
+
249
+ # Ensure that API calls to retrieve term IDs are properly cached.
250
+ mock_get_cover_crop_property_terms.call_count <= 1
251
+ mock_get_irrigated_terms.call_count <= 1
252
+ mock_get_residue_removed_or_burnt_terms.call_count <= 1
253
+
254
+ mock_search.assert_not_called() # Ensure that the term utils are properly mocked.
255
+ assert result == key
256
+
257
+
258
+ @mark.parametrize("key", [1, 2, 3], ids=lambda key: f"scenario-{key}")
259
+ @patch(f"{term_path}.search")
260
+ @patch(f"{utils_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
261
+ @patch(f"{utils_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
262
+ @patch(f"{utils_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
263
+ def test_check_cropland_low_category(
264
+ mock_get_cover_crop_property_terms: MagicMock,
265
+ mock_get_irrigated_terms: MagicMock,
266
+ mock_get_residue_removed_or_burnt_terms: MagicMock,
267
+ mock_search: MagicMock,
268
+ key: int
269
+ ):
270
+ """
271
+ Tests each set of cropland low conditions against a list of nodes that such satisfy it. The function returns the
272
+ key of the matching condition set, which should match the suffix of the fixtures subfolder.
273
+ """
274
+ folder = f"{fixtures_folder}/IpccCarbonInputCategory/cropland-low/scenario-{key}"
275
+
276
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
277
+ site = json.load(f)
278
+
279
+ result = _check_cropland_low_category(**_get_carbon_input_kwargs(site.get("management", [])))
280
+
281
+ # Ensure that API calls to retrieve term IDs are properly cached.
282
+ mock_get_cover_crop_property_terms.call_count <= 1
283
+ mock_get_irrigated_terms.call_count <= 1
284
+ mock_get_residue_removed_or_burnt_terms.call_count <= 1
285
+
286
+ mock_search.assert_not_called() # Ensure that the term utils are properly mocked.
287
+ assert result == key
288
+
289
+
290
+ # subfolder, management_category, expected
291
+ CARBON_INPUT_CATEGORY_PARAMS = [
292
+ (
293
+ "cropland-high-with-manure",
294
+ IpccManagementCategory.FULL_TILLAGE,
295
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE
296
+ ),
297
+ (
298
+ "cropland-high-without-manure/organic-fertiliser", # Closes issue 743
299
+ IpccManagementCategory.FULL_TILLAGE,
300
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE
301
+ ),
302
+ (
303
+ "cropland-high-without-manure/soil-amendment", # Closes issue 743
304
+ IpccManagementCategory.FULL_TILLAGE,
305
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE
306
+ ),
307
+ ("cropland-low/scenario-1", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_LOW),
308
+ ("cropland-low/scenario-2", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_LOW),
309
+ ("cropland-low/scenario-3", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_LOW),
310
+ ("cropland-medium/scenario-1", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
311
+ ("cropland-medium/scenario-2", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
312
+ ("cropland-medium/scenario-3", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
313
+ ("cropland-medium/scenario-4", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
314
+ ("grassland-high", IpccManagementCategory.IMPROVED_GRASSLAND, IpccCarbonInputCategory.GRASSLAND_HIGH),
315
+ (
316
+ "grassland-medium/0-improvements",
317
+ IpccManagementCategory.IMPROVED_GRASSLAND,
318
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM
319
+ ),
320
+ (
321
+ "grassland-medium/1-improvements",
322
+ IpccManagementCategory.IMPROVED_GRASSLAND,
323
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM
324
+ )
325
+ ]
326
+
327
+
328
+ @mark.parametrize(
329
+ "subfolder, management_category, expected",
330
+ CARBON_INPUT_CATEGORY_PARAMS,
331
+ ids=[params[0] for params in CARBON_INPUT_CATEGORY_PARAMS]
332
+ )
333
+ @patch(f"{term_path}.search")
334
+ @patch(f"{utils_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
335
+ @patch(f"{utils_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
336
+ @patch(f"{utils_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
337
+ def test_assign_ipcc_carbon_input_category(
338
+ mock_get_cover_crop_property_terms: MagicMock,
339
+ mock_get_irrigated_terms: MagicMock,
340
+ mock_get_residue_removed_or_burnt_terms: MagicMock,
341
+ mock_search: MagicMock,
342
+ subfolder: str,
343
+ management_category: IpccManagementCategory,
344
+ expected: IpccCarbonInputCategory
345
+ ):
346
+ folder = f"{fixtures_folder}/IpccCarbonInputCategory/{subfolder}"
347
+
348
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
349
+ site = json.load(f)
350
+
351
+ result = _assign_ipcc_carbon_input_category(site.get("management", []), management_category)
352
+
353
+ # Ensure that API calls to retrieve term IDs are properly cached.
354
+ mock_get_cover_crop_property_terms.call_count <= 1
355
+ mock_get_irrigated_terms.call_count <= 1
356
+ mock_get_residue_removed_or_burnt_terms.call_count <= 1
357
+
358
+ mock_search.assert_not_called() # Ensure that the term utils are properly mocked.
359
+ assert result == expected
360
+
361
+
362
+ TIMESTAMPS_CALC_SOC_STOCK = [1990, 1995, 2000, 2005, 2010, 2015, 2020]
363
+
364
+ # regime_start_years, soc_equilibriums, expected
365
+ PARAMS_CALC_SOC_STOCK = [
366
+ (
367
+ [1970, 1995, 1995, 1995, 1995, 1995, 1995],
368
+ array([[77.000], [70.840], [70.840], [70.840], [70.840], [70.840], [70.840]]),
369
+ array([[77.000], [75.460], [73.920], [72.380], [70.840], [70.840], [70.840]])
370
+ ),
371
+ (
372
+ [1970, 1995, 1995, 1995, 2010, 2010, 2010],
373
+ array([[77.000], [70.840], [70.840], [70.840], [80.850], [80.850], [80.850]]),
374
+ array([[77.000], [75.460], [73.920], [72.380], [74.498], [76.615], [78.733]])
375
+ ),
376
+ (
377
+ [1970, 1995, 1995, 1995, 1995, 2015, 2015],
378
+ array([[80.850], [70.840], [70.840], [70.840], [70.840], [80.850], [80.850]]),
379
+ array([[80.850], [78.348], [75.845], [73.343], [70.840], [73.343], [75.845]])
380
+ ),
381
+ (
382
+ [1970, 1970, 2000, 2000, 2000, 2000, 2000],
383
+ array([[80.850], [80.850], [77.000], [77.000], [77.000], [77.000], [77.000]]),
384
+ array([[80.850], [80.850], [79.888], [78.925], [77.963], [77.000], [77.000]])
385
+ ),
386
+ (
387
+ [1970, 1970, 1970, 1970, 2010, 2010, 2010],
388
+ array([[70.840], [70.840], [70.840], [70.840], [80.850], [80.850], [80.850]]),
389
+ array([[70.840], [70.840], [70.840], [70.840], [73.343], [75.845], [78.348]])
390
+ ),
391
+ (
392
+ [1970, 1970, 2000, 2000, 2000, 2015, 2020],
393
+ array([[70.840], [70.840], [80.850], [80.850], [80.850], [70.840], [80.850]]),
394
+ array([[70.840], [70.840], [73.343], [75.845], [78.348], [76.471], [77.565]])
395
+ )
396
+ ]
397
+ IDS_CALC_SOC_STOCK = [f"land-unit-{i+1}" for i in range(len(PARAMS_CALC_SOC_STOCK))]
398
+
399
+
400
+ @mark.parametrize("regime_start_years, soc_equilibriums, expected", PARAMS_CALC_SOC_STOCK, ids=IDS_CALC_SOC_STOCK)
401
+ def test_calc_soc_stocks(regime_start_years, soc_equilibriums, expected):
402
+ """
403
+ Test the interpolation between SOC equilibriums using test data provided in IPCC (2019).
404
+ """
405
+ result = _calc_soc_stocks(
406
+ TIMESTAMPS_CALC_SOC_STOCK, regime_start_years, soc_equilibriums
407
+ )
408
+ assert_array_almost_equal(result, expected, decimal=3)
409
+
410
+
411
+ TEST_INVENTORY = {
412
+ 1960: {
413
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.FOREST,
414
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.OTHER,
415
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.OTHER
416
+ },
417
+ 1965: {
418
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.FOREST,
419
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.OTHER,
420
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.OTHER
421
+ },
422
+ 1970: {
423
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.GRASSLAND,
424
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.NOMINALLY_MANAGED,
425
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.OTHER
426
+ },
427
+ 1995: {
428
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.ANNUAL_CROPS,
429
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.FULL_TILLAGE,
430
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.CROPLAND_LOW
431
+ },
432
+ 2003: {
433
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.ANNUAL_CROPS,
434
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.FULL_TILLAGE,
435
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.CROPLAND_MEDIUM
436
+ },
437
+ 2025: {
438
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.ANNUAL_CROPS,
439
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.FULL_TILLAGE,
440
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.CROPLAND_MEDIUM
441
+ },
442
+ 2026: {
443
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.ANNUAL_CROPS,
444
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.REDUCED_TILLAGE,
445
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.CROPLAND_MEDIUM
446
+ }
447
+ }
448
+
449
+ EXPECTED_MISSING_YEARS = {
450
+ 1990: {
451
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.GRASSLAND,
452
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.NOMINALLY_MANAGED,
453
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.OTHER
454
+ },
455
+ 2023: {
456
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory.ANNUAL_CROPS,
457
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory.FULL_TILLAGE,
458
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory.CROPLAND_MEDIUM
459
+ }
460
+ }
461
+
462
+
463
+ def test_calc_missing_equilibrium_years():
464
+ result = _calc_missing_equilibrium_years(TEST_INVENTORY)
465
+ assert result == EXPECTED_MISSING_YEARS
466
+
467
+
468
+ def test_calc_regime_start_years():
469
+ EXPECTED = [1940, 1940, 1970, 1995, 2003, 2003, 2026]
470
+ result = _calc_regime_start_years(TEST_INVENTORY)
471
+ assert result == EXPECTED
@@ -0,0 +1,208 @@
1
+ from numpy import array
2
+ from numpy.testing import assert_array_almost_equal
3
+ from numpy.typing import NDArray
4
+ from pytest import mark
5
+
6
+ from hestia_earth.models.utils.array_builders import discrete_uniform_2d, repeat_single
7
+
8
+ from hestia_earth.models.ipcc2019.organicCarbonPerHa import MODEL, TERM_ID
9
+ from hestia_earth.models.ipcc2019.organicCarbonPerHa_tier_2_utils import (
10
+ _calc_temperature_factor_annual, _calc_water_factor_annual, _Parameter, _sample_parameter
11
+ )
12
+
13
+ from tests.utils import fixtures_path
14
+
15
+ class_path = f"hestia_earth.models.{MODEL}.{TERM_ID}_tier_2_utils"
16
+ utils_path = f"hestia_earth.models.{MODEL}.{TERM_ID}_utils"
17
+ term_path = "hestia_earth.models.utils.term"
18
+ property_path = "hestia_earth.models.utils.property"
19
+
20
+ fixtures_folder = f"{fixtures_path}/{MODEL}/{TERM_ID}"
21
+
22
+ ITERATIONS = 1000
23
+ SEED = 0
24
+ YEARS = 100
25
+ MONTHS = 12
26
+
27
+
28
+ CROP_RESIDUE_INCORP_TERM_IDS = [
29
+ "aboveGroundCropResidueIncorporated",
30
+ "aboveGroundCropResidueLeftOnField",
31
+ "belowGroundCropResidue",
32
+ "discardedCropIncorporated",
33
+ "discardedCropLeftOnField"
34
+ ]
35
+
36
+ IRRIGATED_TERM_IDS = [
37
+ "rainfedDeepWater",
38
+ "rainfedDeepWaterWaterDepth100Cm",
39
+ "rainfedDeepWaterWaterDepth50100Cm",
40
+ "irrigatedTypeUnspecified",
41
+ "irrigatedCenterPivotIrrigation",
42
+ "irrigatedContinuouslyFlooded",
43
+ "irrigatedDripIrrigation",
44
+ "irrigatedFurrowIrrigation",
45
+ "irrigatedLateralMoveIrrigation",
46
+ "irrigatedLocalizedIrrigation",
47
+ "irrigatedManualIrrigation",
48
+ "irrigatedSurfaceIrrigationMultipleDrainagePeriods",
49
+ "irrigatedSurfaceIrrigationSingleDrainagePeriod",
50
+ "irrigatedSprinklerIrrigation",
51
+ "irrigatedSubIrrigation",
52
+ "irrigatedSurfaceIrrigationDrainageRegimeUnspecified"
53
+ ]
54
+
55
+
56
+ UPLAND_RICE_LAND_COVER_TERM_IDS = [
57
+ "ricePlantUpland"
58
+ ]
59
+
60
+ UPLAND_RICE_CROP_TERM_IDS = [
61
+ "riceGrainInHuskUpland"
62
+ ]
63
+
64
+ DEFAULT_PROPERTIES = {
65
+ "manureDryKgMass": {
66
+ "carbonContent": {
67
+ "value": 38.4
68
+ },
69
+ "nitrogenContent": {
70
+ "value": 2.65
71
+ },
72
+ "ligninContent": {
73
+ "value": 9.67
74
+ }
75
+ }
76
+ }
77
+
78
+
79
+ def fake_find_term_property(term: dict, property: str, *_):
80
+ term_id = term.get('@id', None)
81
+ return DEFAULT_PROPERTIES.get(term_id, {}).get(property, {})
82
+
83
+
84
+ def fake_calc_descriptive_stats(arr: NDArray, *_args, **_kwargs):
85
+ return {"value": [row[0] for row in arr]}
86
+
87
+
88
+ def assert_elements_between(arr: NDArray, min: float, max: float):
89
+ assert ((min <= arr) & (arr <= max)).all()
90
+
91
+
92
+ def assert_rows_unique(arr: NDArray):
93
+ """
94
+ Covert array to a set to remove repeated rows and check that number remaining rows is the same as the number of
95
+ original rows.
96
+ """
97
+ assert len(set(map(tuple, arr))) == len(arr)
98
+
99
+
100
+ PARAMS_SAMPLE_PARAMETER = [p for p in _Parameter]
101
+ IDS_SAMPLE_PARAMETER = [p.name for p in _Parameter]
102
+
103
+
104
+ @mark.parametrize("parameter", PARAMS_SAMPLE_PARAMETER, ids=IDS_SAMPLE_PARAMETER)
105
+ def test_sample_parameter(parameter):
106
+ """
107
+ Check that every parameter can be sampled without raising an error.
108
+ """
109
+ EXPECTED_SHAPE = (1, ITERATIONS)
110
+ result = _sample_parameter(ITERATIONS, parameter)
111
+ assert result.shape == EXPECTED_SHAPE
112
+
113
+
114
+ # temperature_monthly, expected
115
+ PARAMS_TEMPERATURE_FACTOR = [
116
+ (array([[-100] for _ in range(12)]), array([[1.4946486e-27]])),
117
+ (array([[0] for _ in range(12)]), array([[0.0803555]])),
118
+ (array([[33.69] for _ in range(12)]), array([[1]])),
119
+ (array([[45] for _ in range(12)]), array([[0]])),
120
+ (array([[50] for _ in range(12)]), array([[0]])),
121
+ (
122
+ array([
123
+ [22.71129032258065], [20.310714285714287], [19.479032258064514],
124
+ [14.993333333333334], [11.206451612903225], [9.055],
125
+ [8.008064516129034], [11.254838709677419], [11.276666666666666],
126
+ [14.148387096774192], [19.980000000000004], [16.372580645161293]
127
+ ]),
128
+ array([[0.4904241436936742]])
129
+ )
130
+ ]
131
+ IDS_TEMPERATURE_FACTOR = ["-100", "0", "33.69", "45", "50", "ipcc"]
132
+
133
+
134
+ @mark.parametrize("temperature_monthly, expected", PARAMS_TEMPERATURE_FACTOR, ids=IDS_TEMPERATURE_FACTOR)
135
+ def test_calc_annual_temperature_factors(temperature_monthly, expected):
136
+ result = _calc_temperature_factor_annual(temperature_monthly)
137
+ assert_array_almost_equal(result, expected)
138
+
139
+
140
+ def test_calc_annual_temperature_factors_random():
141
+ SHAPE = (YEARS * MONTHS, ITERATIONS)
142
+ MIN, MAX = 0, 1
143
+
144
+ TEMPERATURE_MONTHLY = discrete_uniform_2d(SHAPE, -60, 60, seed=SEED)
145
+
146
+ result = _calc_temperature_factor_annual(TEMPERATURE_MONTHLY)
147
+
148
+ assert_elements_between(result, MIN, MAX)
149
+ assert_rows_unique(result)
150
+ assert result.shape == (YEARS, ITERATIONS)
151
+
152
+
153
+ # precipitation_monthly, pet_monthly, expected
154
+ PARAMS_WATER_FACTOR = [
155
+ (array([[0] for _ in range(12)]), array([[0] for _ in range(12)]), array([[2.24942813]])), # Closes issue 771
156
+ (array([[1] for _ in range(12)]), array([[10000] for _ in range(12)]), array([[0.3195496]])),
157
+ (array([[10000] for _ in range(12)]), array([[1] for _ in range(12)]), array([[2.24942813]])),
158
+ (
159
+ array([
160
+ [4.8], [23.900000000000002], [24.7],
161
+ [3.5999999999999996], [11.3], [11.200000000000001],
162
+ [27.400000000000006], [53], [30.7],
163
+ [39.3], [9.399999999999999], [41.8]
164
+ ]),
165
+ array([
166
+ [253.80000000000007], [214.4], [176.59999999999994],
167
+ [104.19999999999997], [62.79999999999997], [41.59999999999999],
168
+ [45.60000000000001], [64.80000000000001], [91.60000000000001],
169
+ [140.00000000000006], [189.99999999999994], [233.00000000000006]
170
+ ]),
171
+ array([[0.7793321739983536]])
172
+ )
173
+ ]
174
+ IDS_WATER_FACTOR = ["0/0", "1/10000", "10000/1", "ipcc"]
175
+
176
+
177
+ @mark.parametrize("precipitation_monthly, pet_monthly, expected", PARAMS_WATER_FACTOR, ids=IDS_WATER_FACTOR)
178
+ def test_calc_calc_annual_water_factors(precipitation_monthly, pet_monthly, expected):
179
+ result = _calc_water_factor_annual(precipitation_monthly, pet_monthly)
180
+ assert_array_almost_equal(result, expected)
181
+
182
+
183
+ def test_calc_calc_annual_water_factors_random():
184
+ SHAPE = (YEARS * MONTHS, ITERATIONS)
185
+ MIN, MAX = 0.31935, 2.24942813
186
+
187
+ PRECIPITATION_MONTHLY = discrete_uniform_2d(SHAPE, 0, 1000, seed=SEED)
188
+ PET_MONTHLY = discrete_uniform_2d(SHAPE, 0, 2500, seed=SEED+1)
189
+
190
+ result = _calc_water_factor_annual(PRECIPITATION_MONTHLY, PET_MONTHLY)
191
+
192
+ assert_elements_between(result, MIN, MAX)
193
+ assert_rows_unique(result)
194
+ assert result.shape == (YEARS, ITERATIONS)
195
+
196
+
197
+ def test_calc_calc_annual_water_factors_irrigated():
198
+ EXPECTED = 0.775 * 1.5
199
+
200
+ SHAPE = (YEARS * MONTHS, ITERATIONS)
201
+ PRECIPITATION_MONTHLY = discrete_uniform_2d(SHAPE, 0, 1000, seed=SEED)
202
+ PET_MONTHLY = discrete_uniform_2d(SHAPE, 0, 2500, seed=SEED+1)
203
+ IRRIGATED_MONTHLY = repeat_single(SHAPE, True)
204
+
205
+ result = _calc_water_factor_annual(PRECIPITATION_MONTHLY, PET_MONTHLY, IRRIGATED_MONTHLY)
206
+
207
+ (result == EXPECTED).all() # assert all elements in result are the expected value
208
+ assert result.shape == (YEARS, ITERATIONS)