hestia-earth-models 0.58.0__py3-none-any.whl → 0.59.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 (34) hide show
  1. hestia_earth/models/cycle/{irrigated.py → irrigatedTypeUnspecified.py} +4 -4
  2. hestia_earth/models/cycle/residueIncorporated.py +1 -1
  3. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +2 -2
  4. hestia_earth/models/geospatialDatabase/clayContent.py +17 -4
  5. hestia_earth/models/geospatialDatabase/sandContent.py +17 -4
  6. hestia_earth/models/impact_assessment/irrigated.py +0 -3
  7. hestia_earth/models/ipcc2019/co2ToAirSoilCarbonStockChangeManagementChange.py +10 -9
  8. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +4 -51
  9. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserDirect.py +104 -0
  10. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserDirect.py +105 -0
  11. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +1059 -1220
  12. hestia_earth/models/ipcc2019/utils.py +82 -1
  13. hestia_earth/models/mocking/search-results.json +161 -87
  14. hestia_earth/models/site/management.py +12 -9
  15. hestia_earth/models/site/organicCarbonPerHa.py +251 -89
  16. hestia_earth/models/utils/blank_node.py +157 -34
  17. hestia_earth/models/utils/cycle.py +6 -3
  18. hestia_earth/models/utils/measurement.py +1 -1
  19. hestia_earth/models/utils/term.py +46 -1
  20. hestia_earth/models/version.py +1 -1
  21. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/METADATA +4 -8
  22. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/RECORD +34 -30
  23. tests/models/cycle/{test_irrigated.py → test_irrigatedTypeUnspecified.py} +1 -1
  24. tests/models/geospatialDatabase/test_clayContent.py +9 -3
  25. tests/models/geospatialDatabase/test_sandContent.py +9 -3
  26. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserDirect.py +74 -0
  27. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserDirect.py +74 -0
  28. tests/models/ipcc2019/test_organicCarbonPerHa.py +303 -1044
  29. tests/models/site/test_organicCarbonPerHa.py +51 -5
  30. tests/models/utils/test_blank_node.py +102 -42
  31. tests/models/utils/test_term.py +17 -3
  32. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/LICENSE +0 -0
  33. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/WHEEL +0 -0
  34. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/top_level.txt +0 -0
@@ -1,24 +1,12 @@
1
- from copy import deepcopy
2
1
  from unittest.mock import patch
3
2
  import json
4
3
  from numpy.testing import assert_almost_equal
4
+ from pytest import mark
5
5
  import random
6
- from typing import (
7
- NamedTuple,
8
- Optional,
9
- Union
10
- )
11
- from hestia_earth.schema import (
12
- SiteSiteType,
13
- TermTermType
14
- )
15
6
 
16
7
  from tests.utils import (
17
8
  fixtures_path,
18
9
  fake_new_measurement,
19
- PROPERTY,
20
- MANAGEMENT,
21
- MEASUREMENT,
22
10
  )
23
11
  from hestia_earth.models.ipcc2019.organicCarbonPerHa import (
24
12
  _assign_ipcc_carbon_input_category,
@@ -28,45 +16,19 @@ from hestia_earth.models.ipcc2019.organicCarbonPerHa import (
28
16
  _calc_temperature_factor,
29
17
  _calc_tier_1_soc_stocks,
30
18
  _calc_water_factor,
31
- _check_cropland_high_with_manure_category,
32
- _check_cropland_high_without_manure_category,
33
19
  _check_cropland_low_category,
34
20
  _check_cropland_medium_category,
35
- _make_carbon_input_args,
36
- _run_annual_organic_carbon_inputs,
37
- CarbonSource,
38
- CLAY_CONTENT_MAX,
39
- CLAY_CONTENT_TERM_ID,
21
+ _get_carbon_input_kwargs,
22
+ _iterate_soc_equilibriums,
40
23
  IpccCarbonInputCategory,
41
24
  IpccLandUseCategory,
42
25
  IpccManagementCategory,
43
26
  IpccSoilCategory,
44
27
  MODEL,
45
28
  run,
46
- SAND_CONTENT_MIN,
47
- SAND_CONTENT_TERM_ID,
48
29
  TERM_ID
49
30
  )
50
31
 
51
-
52
- TIER_2_SUBFOLDERS = [
53
- 'Tier2/with-generalised-monthly-measurements', # Closes issue 600
54
- 'Tier2/with-incomplete-climate-data', # Closes issue 599
55
- 'Tier2/with-initial-soc',
56
- 'Tier2/with-multi-year-cycles',
57
- 'Tier2/without-any-measurements', # Closes issue 594
58
- 'Tier2/without-initial-soc'
59
- ]
60
-
61
- TIER_1_SUBFOLDERS = [
62
- 'Tier1/cropland-with-measured-soc',
63
- 'Tier1/cropland-without-measured-soc',
64
- 'Tier1/permanent-pasture',
65
- 'Tier1/should-not-run',
66
- 'Tier1/without-management-with-measured-soc',
67
- 'Tier1/cropland-depth-as-float'
68
- ]
69
-
70
32
  class_path = f"hestia_earth.models.{MODEL}.{TERM_ID}"
71
33
  fixtures_folder = f"{fixtures_path}/{MODEL}/{TERM_ID}"
72
34
 
@@ -87,10 +49,10 @@ CROP_RESIDUE_INCORP_TERM_IDS = [
87
49
  ]
88
50
 
89
51
  IRRIGATED_TERM_IDS = [
90
- "deepWater",
91
- "deepWaterWaterDepth100Cm",
92
- "deepWaterWaterDepth50100Cm",
93
- "irrigated",
52
+ "rainfedDeepWater",
53
+ "rainfedDeepWaterWaterDepth100Cm",
54
+ "rainfedDeepWaterWaterDepth50100Cm",
55
+ "irrigatedTypeUnspecified",
94
56
  "irrigatedCenterPivotIrrigation",
95
57
  "irrigatedContinuouslyFlooded",
96
58
  "irrigatedDripIrrigation",
@@ -98,11 +60,16 @@ IRRIGATED_TERM_IDS = [
98
60
  "irrigatedLateralMoveIrrigation",
99
61
  "irrigatedLocalizedIrrigation",
100
62
  "irrigatedManualIrrigation",
101
- "irrigatedMultipleDrainagePeriods",
102
- "irrigatedSingleDrainagePeriod",
63
+ "irrigatedSurfaceIrrigationMultipleDrainagePeriods",
64
+ "irrigatedSurfaceIrrigationSingleDrainagePeriod",
103
65
  "irrigatedSprinklerIrrigation",
104
66
  "irrigatedSubIrrigation",
105
- "irrigatedSurfaceIrrigation"
67
+ "irrigatedSurfaceIrrigationDrainageRegimeUnspecified"
68
+ ]
69
+
70
+ LONG_FALLOW_LAND_COVER_TERM_IDS = [
71
+ "longFallow",
72
+ "longBareFallow"
106
73
  ]
107
74
 
108
75
  RESIDUE_REMOVED_OR_BURNT_TERM_IDS = [
@@ -110,91 +77,95 @@ RESIDUE_REMOVED_OR_BURNT_TERM_IDS = [
110
77
  "residueRemoved"
111
78
  ]
112
79
 
113
- RICE_PLANT_UPLAND_TERM_IDS = [
80
+ UPLAND_RICE_LAND_COVER_TERM_IDS = [
114
81
  "ricePlantUpland"
115
82
  ]
116
83
 
117
- # --- TIER 2 TEST UTILS ---
84
+ UPLAND_RICE_CROP_TERM_IDS = [
85
+ "riceGrainInHuskUpland"
86
+ ]
118
87
 
119
88
 
120
- def _load_cycles(path):
121
- with open(path, encoding='utf-8') as f:
122
- cycles = json.load(f)
123
- return cycles
89
+ # --- TIER 1 & TIER 2 TESTS ---
124
90
 
125
91
 
126
- # --- TIER 1 TEST UTILS ---
92
+ # subfolder, load_cycles
93
+ RUN_SUBFOLDERS = [
94
+ ("tier-1-and-2/cropland", True),
95
+ ("tier-2/with-generalised-monthly-measurements", True), # Closes issue 600
96
+ ("tier-2/with-incomplete-climate-data", True), # Closes issue 599
97
+ ("tier-2/with-initial-soc", True),
98
+ ("tier-2/with-multi-year-cycles", True),
99
+ ("tier-2/without-any-measurements", True), # Closes issue 594
100
+ ("tier-2/without-initial-soc", True),
101
+ ("tier-2/with-irrigation", True),
102
+ ("tier-2/with-irrigation-dates", True),
103
+ ("tier-2/with-paddy-rice", True),
104
+ ("tier-2/with-irrigated-upland-rice", True),
105
+ ("tier-1/cropland-depth-as-float", False),
106
+ ("tier-1/cropland-with-measured-soc", False),
107
+ ("tier-1/cropland-without-measured-soc", False),
108
+ ("tier-1/permanent-pasture", False),
109
+ ("tier-1/should-not-run", False),
110
+ ("tier-1/without-management-with-measured-soc", False)
111
+ ]
127
112
 
128
113
 
129
- def fake_measurement(
130
- term_id: str,
131
- value: list,
132
- term_type: TermTermType,
133
- properties: Optional[list[dict]] = None
134
- ):
135
- node = deepcopy(MEASUREMENT)
136
- node['term']['@id'] = term_id
137
- node['term']['termType'] = term_type.value
138
- node['value'] = (
139
- value if isinstance(value, list) else [value]
140
- )
141
- if properties:
142
- node['properties'] = (
143
- properties if isinstance(properties, list) else [properties]
144
- )
145
- return node
146
-
147
-
148
- def fake_management(
149
- term_id: str,
150
- value: Union[float, bool],
151
- term_type: TermTermType,
152
- properties: Optional[list[dict]] = None
114
+ @mark.parametrize(
115
+ "subfolder, load_cycles",
116
+ RUN_SUBFOLDERS,
117
+ ids=[params[0] for params in RUN_SUBFOLDERS]
118
+ )
119
+ @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
120
+ @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
121
+ @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
122
+ @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
123
+ @patch(f"{class_path}.get_long_fallow_land_cover_terms", return_value=LONG_FALLOW_LAND_COVER_TERM_IDS)
124
+ @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
125
+ @patch(f"{class_path}.get_upland_rice_land_cover_terms", return_value=UPLAND_RICE_LAND_COVER_TERM_IDS)
126
+ @patch(f"{class_path}.get_upland_rice_crop_terms", return_value=UPLAND_RICE_CROP_TERM_IDS)
127
+ @patch(f"{class_path}.related_cycles")
128
+ def test_run(
129
+ mock_related_cycles,
130
+ _mock_get_upland_rice_crop_terms,
131
+ _mock_get_upland_rice_land_cover_terms,
132
+ _mock_get_residue_removed_or_burnt_terms,
133
+ _mock_get_long_fallow_land_cover_terms,
134
+ _mock_get_irrigated_terms,
135
+ _mock_get_crop_residue_incorporated_or_left_on_field_terms,
136
+ _mock_get_cover_crop_property_terms,
137
+ _mock_new_measurement,
138
+ subfolder,
139
+ load_cycles
153
140
  ):
154
- node = deepcopy(MANAGEMENT)
155
- node['term']['@id'] = term_id
156
- node['term']['termType'] = term_type.value
157
- node['value'] = (
158
- value[0] if isinstance(value, list) else value
159
- )
160
- if properties:
161
- node['properties'] = (
162
- properties if isinstance(properties, list) else [properties]
163
- )
164
- return node
141
+ folder = f"{fixtures_folder}/{subfolder}"
165
142
 
143
+ def load_cycles_from_file():
144
+ with open(f"{folder}/cycles.jsonld", encoding='utf-8') as f:
145
+ return json.load(f)
166
146
 
167
- def fake_property(
168
- term_id: str,
169
- value: Union[float, bool],
170
- ):
171
- node = deepcopy(PROPERTY)
172
- node['term']['@id'] = term_id
173
- node['term']['termType'] = TermTermType.PROPERTY.value
174
- node['value'] = (
175
- value[0] if isinstance(value, list) else value
176
- )
177
- return node
147
+ mock_related_cycles.return_value = load_cycles_from_file() if load_cycles else []
178
148
 
149
+ with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
150
+ site = json.load(f)
179
151
 
180
- Tier1SocStockFixtures = NamedTuple('Tier1SocStockFixtures', [
181
- ('timestamps', list[int]),
182
- ('soc_equilibriums', list[float]),
183
- ('expected', list[float])
184
- ])
185
-
152
+ with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
153
+ expected = json.load(f)
186
154
 
187
- # --- TIER 1 & TIER 2 TESTS ---
155
+ result = run(site)
156
+ assert result == expected
188
157
 
189
158
 
190
159
  @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
191
160
  @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
192
161
  @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
193
162
  @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
163
+ @patch(f"{class_path}.get_long_fallow_land_cover_terms", return_value=LONG_FALLOW_LAND_COVER_TERM_IDS)
194
164
  @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
195
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
165
+ @patch(f"{class_path}.get_upland_rice_land_cover_terms", return_value=UPLAND_RICE_LAND_COVER_TERM_IDS)
166
+ @patch(f"{class_path}.get_upland_rice_crop_terms", return_value=UPLAND_RICE_CROP_TERM_IDS)
196
167
  @patch(f"{class_path}.related_cycles", return_value=[])
197
- def test_run_empty_site_and_cycles(*args):
168
+ def test_run_no_data(*args):
198
169
  SITE = {}
199
170
  EXPECTED = []
200
171
  result = run(SITE)
@@ -238,1012 +209,300 @@ def test_calc_water_factor():
238
209
  assert _calc_water_factor(1, 1) == _calc_water_factor(1000, 1000)
239
210
 
240
211
 
241
- def test_run_annual_organic_carbon_inputs():
242
- """
243
- Test the _run_annual_organic_carbon_inputs model:
244
-
245
- As the IPCC don't provide any test data, we can generate some random inputs and test that the results
246
- fall within the minimum and maximum bounds.
247
- """
248
- NUM_YEARS = 9999
249
- MIN_SOURCES, MAX_SOURCES = 0, 99
250
- MIN_MASS, MAX_MASS = 0, 9999
251
- MIN_C, MAX_C = 0.1, 0.5
252
- MIN_N, MAX_N = 0.001, 0.01
253
- MIN_LIG, MAX_LIG = 0.01, 0.1
254
-
255
- min_c_input = MIN_MASS * MIN_C * MIN_SOURCES
256
- max_c_input = MAX_MASS * MAX_C * MAX_SOURCES
257
-
258
- def generate_random_carbon_sources():
259
- return [
260
- CarbonSource(
261
- mass=random.uniform(MIN_MASS, MAX_MASS),
262
- carbon_content=random.uniform(MIN_C, MAX_C),
263
- nitrogen_content=random.uniform(MIN_N, MAX_N),
264
- lignin_content=random.uniform(MIN_LIG, MAX_LIG)
265
- ) for _ in range(0, random.randint(MIN_SOURCES, MAX_SOURCES))
266
- ]
267
-
268
- timestamps = list(range(0, NUM_YEARS))
269
- annual_carbon_sources = [
270
- generate_random_carbon_sources() for _ in timestamps
271
- ]
272
-
273
- result = _run_annual_organic_carbon_inputs(
274
- timestamps,
275
- annual_carbon_sources
276
- )
277
-
278
- for i in range(0, NUM_YEARS):
279
- assert result.timestamps[i] == timestamps[i]
280
- assert min_c_input <= result.organic_carbon_inputs[i] <= max_c_input
281
- assert MIN_N <= result.average_nitrogen_contents[i] <= MAX_N
282
- assert MIN_LIG <= result.average_lignin_contents[i] <= MAX_LIG
283
-
284
-
285
- # --- TIER 2 TESTS: SOC MODEL ---
286
-
287
-
288
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
289
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
290
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
291
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
292
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
293
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
294
- @patch(f"{class_path}.related_cycles")
295
- def test_run_tier_2_with_generalised_monthly_measurements(mock_related_cycles, *args):
296
- """
297
- Test for sites with monthly climate measurements (e.g., `precipitationMonthly`) with dates in the format `--MM`.
298
-
299
- Tier 2 model should not run, as monthly climate measurements must be associated with a year and a month in the
300
- format `YYYY-MM`. Therefore, `run` function is expected to return an empty list `[]`.
301
- """
302
-
303
- SUBFOLDER = TIER_2_SUBFOLDERS[0]
304
- folder = f"{fixtures_folder}/{SUBFOLDER}"
305
-
306
- mock_related_cycles.return_value = _load_cycles(f"{folder}/cycles.jsonld")
307
-
308
- with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
309
- site = json.load(f)
310
-
311
- result = run(site)
312
- assert result == []
212
+ # --- IPCC SOIL CATEGORY TESTS ---
313
213
 
314
214
 
315
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
316
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
317
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
318
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
319
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
320
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
321
- @patch(f"{class_path}.related_cycles")
322
- def test_run_tier_2_with_incomplete_climate_data(mock_related_cycles, *args):
215
+ # subfolder, expected
216
+ SOIL_CATEGORY_PARAMS = [
217
+ ("fractional", IpccSoilCategory.WETLAND_SOILS),
218
+ ("no-measurements", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS),
219
+ ("sandy-override", IpccSoilCategory.SANDY_SOILS),
220
+ ("soilType/hac", IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS),
221
+ ("soilType/lac", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS),
222
+ ("soilType/org", IpccSoilCategory.ORGANIC_SOILS),
223
+ ("soilType/pod", IpccSoilCategory.SPODIC_SOILS),
224
+ ("soilType/san", IpccSoilCategory.SANDY_SOILS),
225
+ ("soilType/vol", IpccSoilCategory.VOLCANIC_SOILS),
226
+ ("soilType/wet", IpccSoilCategory.WETLAND_SOILS),
227
+ ("usdaSoilType/hac", IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS),
228
+ ("usdaSoilType/lac", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS),
229
+ ("usdaSoilType/org", IpccSoilCategory.ORGANIC_SOILS),
230
+ ("usdaSoilType/pod", IpccSoilCategory.SPODIC_SOILS),
231
+ ("usdaSoilType/san", IpccSoilCategory.SANDY_SOILS),
232
+ ("usdaSoilType/vol", IpccSoilCategory.VOLCANIC_SOILS),
233
+ ("usdaSoilType/wet", IpccSoilCategory.WETLAND_SOILS)
234
+ ]
323
235
 
324
- SUBFOLDER = TIER_2_SUBFOLDERS[1]
325
- folder = f"{fixtures_folder}/{SUBFOLDER}"
326
236
 
327
- mock_related_cycles.return_value = _load_cycles(f"{folder}/cycles.jsonld")
237
+ @mark.parametrize(
238
+ "subfolder, expected",
239
+ SOIL_CATEGORY_PARAMS,
240
+ ids=[params[0] for params in SOIL_CATEGORY_PARAMS]
241
+ )
242
+ def test_assign_ipcc_soil_category(subfolder, expected):
243
+ folder = f"{fixtures_folder}/IpccSoilCategory/{subfolder}"
328
244
 
329
245
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
330
246
  site = json.load(f)
331
247
 
332
- result = run(site)
333
- assert result == []
334
-
335
-
336
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
337
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
338
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
339
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
340
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
341
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
342
- @patch(f"{class_path}.related_cycles")
343
- def test_run_tier_2_with_initial_soc(mock_related_cycles, *args):
248
+ result = _assign_ipcc_soil_category(site.get("measurements", []))
249
+ assert result == expected
344
250
 
345
- SUBFOLDER = TIER_2_SUBFOLDERS[2]
346
- folder = f"{fixtures_folder}/{SUBFOLDER}"
347
251
 
348
- mock_related_cycles.return_value = _load_cycles(f"{folder}/cycles.jsonld")
252
+ # --- IPCC LAND USE CATEGORY TESTS ---
349
253
 
350
- with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
351
- site = json.load(f)
352
254
 
353
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
354
- expected = json.load(f)
355
-
356
- result = run(site)
357
- assert result == expected
255
+ # subfolder, soil_category, expected
256
+ LAND_USE_CATEGORY_PARAMS = [
257
+ ("annual-crops", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.ANNUAL_CROPS),
258
+ ("annual-crops-wet", IpccSoilCategory.WETLAND_SOILS, IpccLandUseCategory.ANNUAL_CROPS_WET),
259
+ ("forest", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.FOREST),
260
+ ("fractional", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PERENNIAL_CROPS),
261
+ ("grassland", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.GRASSLAND),
262
+ ("irrigated-upland-rice", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PADDY_RICE_CULTIVATION),
263
+ ("native", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.NATIVE),
264
+ ("other", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.OTHER),
265
+ ("paddy-rice-cultivation", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PADDY_RICE_CULTIVATION),
266
+ ("perennial-crops", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.PERENNIAL_CROPS),
267
+ ("set-aside", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.SET_ASIDE),
268
+ ("set-aside-override", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.SET_ASIDE),
269
+ ("upland-rice", IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS, IpccLandUseCategory.ANNUAL_CROPS),
270
+ ]
358
271
 
359
272
 
360
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
361
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
362
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
273
+ @mark.parametrize(
274
+ "subfolder, soil_category, expected",
275
+ LAND_USE_CATEGORY_PARAMS,
276
+ ids=[params[0] for params in LAND_USE_CATEGORY_PARAMS]
277
+ )
363
278
  @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
364
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
365
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
366
- @patch(f"{class_path}.related_cycles")
367
- def test_run_tier_2_with_multi_year_cycles(mock_related_cycles, *args):
368
-
369
- SUBFOLDER = TIER_2_SUBFOLDERS[3]
370
- folder = f"{fixtures_folder}/{SUBFOLDER}"
371
-
372
- mock_related_cycles.return_value = _load_cycles(f"{folder}/cycles.jsonld")
279
+ @patch(f"{class_path}.get_long_fallow_land_cover_terms", return_value=LONG_FALLOW_LAND_COVER_TERM_IDS)
280
+ @patch(f"{class_path}.get_upland_rice_land_cover_terms", return_value=UPLAND_RICE_LAND_COVER_TERM_IDS)
281
+ def test_assign_ipcc_land_use_category(
282
+ _mock_get_upland_rice_land_cover_terms,
283
+ _mock_get_long_fallow_land_cover_terms,
284
+ _mock_get_irrigated_terms,
285
+ subfolder,
286
+ soil_category,
287
+ expected
288
+ ):
289
+ folder = f"{fixtures_folder}/IpccLandUseCategory/{subfolder}"
373
290
 
374
291
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
375
292
  site = json.load(f)
376
293
 
377
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
378
- expected = json.load(f)
379
-
380
- result = run(site)
294
+ result = _assign_ipcc_land_use_category(site.get("siteType"), site.get("management", []), soil_category)
381
295
  assert result == expected
382
296
 
383
297
 
384
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
385
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
386
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
387
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
388
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
389
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
390
- @patch(f"{class_path}.related_cycles")
391
- def test_run_tier_2_without_any_measurements(mock_related_cycles, *args):
392
-
393
- SUBFOLDER = TIER_2_SUBFOLDERS[4]
394
- folder = f"{fixtures_folder}/{SUBFOLDER}"
395
-
396
- mock_related_cycles.return_value = _load_cycles(f"{folder}/cycles.jsonld")
397
-
398
- with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
399
- site = json.load(f)
400
-
401
- result = run(site)
402
- assert result == []
298
+ # --- IPCC MANAGEMENT CATEGORY TESTS ---
403
299
 
404
300
 
405
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
406
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
407
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
408
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
409
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
410
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
411
- @patch(f"{class_path}.related_cycles")
412
- def test_run_tier_2_without_initial_soc(mock_related_cycles, *args):
301
+ # subfolder, land_use_category, expected
302
+ MANAGEMENT_CATEGORY_PARAMS = [
303
+ ("fractional-annual-crops", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.REDUCED_TILLAGE),
304
+ ("fractional-annual-crops-wet", IpccLandUseCategory.ANNUAL_CROPS_WET, IpccManagementCategory.REDUCED_TILLAGE),
305
+ ("fractional-grassland", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.IMPROVED_GRASSLAND),
306
+ ("full-tillage", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.FULL_TILLAGE),
307
+ ("high-intensity-grazing", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.HIGH_INTENSITY_GRAZING),
308
+ ("improved-grassland", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.IMPROVED_GRASSLAND),
309
+ ("no-management/annual-crops", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.FULL_TILLAGE),
310
+ ("no-management/annual-crops-wet", IpccLandUseCategory.ANNUAL_CROPS_WET, IpccManagementCategory.FULL_TILLAGE),
311
+ ("no-management/grassland", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.NOMINALLY_MANAGED),
312
+ ("no-tillage", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.NO_TILLAGE),
313
+ ("nominally-managed", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.NOMINALLY_MANAGED),
314
+ ("other", IpccLandUseCategory.OTHER, IpccManagementCategory.OTHER),
315
+ ("reduced-tillage", IpccLandUseCategory.ANNUAL_CROPS, IpccManagementCategory.REDUCED_TILLAGE),
316
+ ("severely-degraded", IpccLandUseCategory.GRASSLAND, IpccManagementCategory.SEVERELY_DEGRADED),
317
+ ]
413
318
 
414
- SUBFOLDER = TIER_2_SUBFOLDERS[5]
415
- folder = f"{fixtures_folder}/{SUBFOLDER}"
416
319
 
417
- mock_related_cycles.return_value = _load_cycles(f"{folder}/cycles.jsonld")
320
+ @mark.parametrize(
321
+ "subfolder, land_use_category, expected",
322
+ MANAGEMENT_CATEGORY_PARAMS,
323
+ ids=[params[0] for params in MANAGEMENT_CATEGORY_PARAMS]
324
+ )
325
+ def test_assign_ipcc_management_category(subfolder, land_use_category, expected):
326
+ folder = f"{fixtures_folder}/IpccManagementCategory/{subfolder}"
418
327
 
419
328
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
420
329
  site = json.load(f)
421
330
 
422
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
423
- expected = json.load(f)
424
-
425
- result = run(site)
331
+ result = _assign_ipcc_management_category(site.get("management", []), land_use_category)
426
332
  assert result == expected
427
333
 
428
334
 
429
- # --- IPCC SOIL CATEGORY TESTS ---
430
-
431
-
432
- EXPECTED_IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_TERM_IDS = {
433
- IpccSoilCategory.ORGANIC_SOILS: 'histosol',
434
- IpccSoilCategory.SANDY_SOILS: 'arenosols',
435
- IpccSoilCategory.WETLAND_SOILS: 'gleysols',
436
- IpccSoilCategory.VOLCANIC_SOILS: 'andosols',
437
- IpccSoilCategory.SPODIC_SOILS: 'podzols',
438
- IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: 'alisols',
439
- IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: 'cryosols'
440
- }
441
-
442
- EXPECTED_IPCC_SOIL_CATEGORY_TO_USDA_SOIL_TYPE_TERM_IDS = {
443
- IpccSoilCategory.ORGANIC_SOILS: 'histosols',
444
- IpccSoilCategory.SANDY_SOILS: 'psamments',
445
- IpccSoilCategory.WETLAND_SOILS: 'aquicCalcixerepts',
446
- IpccSoilCategory.VOLCANIC_SOILS: 'andisols',
447
- IpccSoilCategory.SPODIC_SOILS: 'spodosols',
448
- IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: 'vertisols',
449
- IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: 'alfisols'
450
- }
451
-
452
-
453
- def test_assign_ipcc_soil_category_standard_cases():
454
-
455
- # soilType
456
- for expected, term_id in EXPECTED_IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_TERM_IDS.items():
457
- measurements = [
458
- fake_measurement(term_id, [100], TermTermType.SOILTYPE)
459
- ]
460
- result = _assign_ipcc_soil_category(measurements)
461
-
462
- assert result == expected
463
-
464
- # usdaSoilType
465
- for expected, term_id in EXPECTED_IPCC_SOIL_CATEGORY_TO_USDA_SOIL_TYPE_TERM_IDS.items():
466
- measurements = [
467
- fake_measurement(term_id, [100], TermTermType.USDASOILTYPE)
468
- ]
469
- result = _assign_ipcc_soil_category(measurements)
470
-
471
- assert result == expected
472
-
473
-
474
- def test_assign_ipcc_soil_category_fractional_case():
475
-
476
- EXPECTED = IpccSoilCategory.WETLAND_SOILS
477
- USDA_SOIL_CATEGORY_TERM_IDS = [
478
- 'aquicCalcixerepts', # WET
479
- 'aquicXeropsamments', # WET
480
- 'aquicFraglossudalfs', # WET
481
- 'vertisols' # HAC
482
- ]
483
-
484
- measurements = [
485
- fake_measurement(term_id, [100/len(USDA_SOIL_CATEGORY_TERM_IDS)], TermTermType.USDASOILTYPE)
486
- for term_id in USDA_SOIL_CATEGORY_TERM_IDS
487
- ]
488
- result = _assign_ipcc_soil_category(measurements)
489
-
490
- assert result == EXPECTED
491
-
492
-
493
- def test_assign_ipcc_soil_category_sandy_override_case():
494
-
495
- EXPECTED = IpccSoilCategory.SANDY_SOILS
496
- LAC_SOIL_TYPE_TERM_ID = (
497
- EXPECTED_IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_TERM_IDS[IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS]
498
- )
499
-
500
- # `soilType` and/or `usdaSoilType` overridden by sand and clay content
501
- measurements = [
502
- fake_measurement(LAC_SOIL_TYPE_TERM_ID, [100], TermTermType.SOILTYPE),
503
- fake_measurement(CLAY_CONTENT_TERM_ID, [CLAY_CONTENT_MAX-1], TermTermType.MEASUREMENT),
504
- fake_measurement(SAND_CONTENT_TERM_ID, [SAND_CONTENT_MIN+1], TermTermType.MEASUREMENT)
505
- ]
506
- result = _assign_ipcc_soil_category(measurements)
507
-
508
- assert result == EXPECTED
509
-
510
-
511
- def test_assign_ipcc_soil_category_no_measurements_case():
512
-
513
- EXPECTED = IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS
514
-
515
- measurements = []
516
- result = _assign_ipcc_soil_category(measurements)
517
-
518
- assert result == EXPECTED
519
-
520
-
521
- # --- IPCC LAND USE CATEGORY TESTS ---
522
-
523
-
524
- EXPECTED_IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE = {
525
- IpccLandUseCategory.GRASSLAND: SiteSiteType.PERMANENT_PASTURE.value,
526
- IpccLandUseCategory.FOREST: SiteSiteType.FOREST.value,
527
- IpccLandUseCategory.SET_ASIDE: SiteSiteType.CROPLAND.value,
528
- IpccLandUseCategory.NATIVE: SiteSiteType.OTHER_NATURAL_VEGETATION.value,
529
- IpccLandUseCategory.OTHER: SiteSiteType.AGRI_FOOD_PROCESSOR,
530
- }
531
-
532
- EXPECTED_IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_TERM_ID = {
533
- IpccLandUseCategory.PERENNIAL_CROPS: 'grapesVine',
534
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: 'ricePlantFlooded',
535
- IpccLandUseCategory.ANNUAL_CROPS: 'kalePlant',
536
- }
537
-
538
- SITE_TYPE_CROPLAND = SiteSiteType.CROPLAND.value
539
- IPCC_SOIL_CATEGORY_LAC = IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS
540
-
541
-
542
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
543
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
544
- def test_assign_ipcc_land_use_category_standard_cases(*args):
545
- # site type only
546
- for expected, site_type in EXPECTED_IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE.items():
547
-
548
- management_nodes = []
549
- result = _assign_ipcc_land_use_category(
550
- site_type,
551
- management_nodes,
552
- IPCC_SOIL_CATEGORY_LAC
553
- )
554
-
555
- assert result == expected
556
-
557
- # site type `cropland` and land cover
558
- for expected, term_id in EXPECTED_IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_TERM_ID.items():
559
-
560
- management_nodes = [
561
- fake_management(
562
- term_id,
563
- 100,
564
- TermTermType.LANDCOVER
565
- )
566
- ]
567
- result = _assign_ipcc_land_use_category(
568
- SITE_TYPE_CROPLAND,
569
- management_nodes,
570
- IPCC_SOIL_CATEGORY_LAC
571
- )
572
-
573
- assert result == expected
574
-
575
-
576
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
577
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
578
- def test_assign_ipcc_land_use_category_fractional_case(*args):
579
- EXPECTED = IpccLandUseCategory.PERENNIAL_CROPS
580
- LAND_COVER_TERM_IDS = [
581
- 'appleTree', # PERENNIAL_CROPS
582
- 'apricotTree', # PERENNIAL_CROPS
583
- 'wheatPlant', # ANNUAL_CROPS
584
- 'barleyPlant' # ANNUAL_CROPS
585
- ]
586
-
587
- management_nodes = [
588
- fake_management(
589
- term_id, 100/len(LAND_COVER_TERM_IDS), TermTermType.LANDCOVER
590
- ) for term_id in LAND_COVER_TERM_IDS
591
- ]
592
- result = _assign_ipcc_land_use_category(
593
- SITE_TYPE_CROPLAND,
594
- management_nodes,
595
- IPCC_SOIL_CATEGORY_LAC
596
- )
597
-
598
- assert result == EXPECTED
599
-
600
-
601
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
602
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
603
- def test_assign_ipcc_land_use_category_annual_crops_wet_case(*args):
604
- EXPECTED = IpccLandUseCategory.ANNUAL_CROPS_WET
605
- IPCC_SOIL_CATEGORY_WET = IpccSoilCategory.WETLAND_SOILS
606
-
607
- management_nodes = [
608
- fake_management(
609
- EXPECTED_IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_TERM_ID[IpccLandUseCategory.ANNUAL_CROPS],
610
- 100,
611
- TermTermType.LANDCOVER
612
- )
613
- ]
614
-
615
- result = _assign_ipcc_land_use_category(
616
- SITE_TYPE_CROPLAND,
617
- management_nodes,
618
- IPCC_SOIL_CATEGORY_WET
619
- )
620
- assert result == EXPECTED
621
-
622
-
623
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
624
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
625
- def test_assign_ipcc_land_use_category_set_aside_case(*args):
626
- EXPECTED = IpccLandUseCategory.SET_ASIDE
627
- WHITE_CLOVER_PLANT_TERM_ID = 'whiteCloverPlant'
628
- LONG_FALLOW_CROP_TERM_ID = 'longFallowCrop'
629
-
630
- management_nodes = [
631
- fake_management(
632
- WHITE_CLOVER_PLANT_TERM_ID,
633
- 100,
634
- TermTermType.LANDCOVER,
635
- properties=[
636
- fake_property(
637
- LONG_FALLOW_CROP_TERM_ID,
638
- True
639
- )
640
- ]
641
- )
642
- ]
643
-
644
- result = _assign_ipcc_land_use_category(
645
- SITE_TYPE_CROPLAND,
646
- management_nodes,
647
- IPCC_SOIL_CATEGORY_LAC
648
- )
649
-
650
- assert result == EXPECTED
651
-
652
-
653
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
654
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
655
- def test_assign_ipcc_land_use_category_upland_rice_cases(*args):
656
- EXPECTED_IRRIGATED = IpccLandUseCategory.PADDY_RICE_CULTIVATION
657
- EXPECTED_NOT_IRRIGATED = IpccLandUseCategory.ANNUAL_CROPS
658
-
659
- RICE_PLANT_UPLAND_TERM_ID = 'ricePlantUpland'
660
- IRRIGATED_TERM_ID = 'irrigated'
661
-
662
- # Irrigated upland rice
663
- management_nodes_irrigated = [
664
- fake_management(
665
- RICE_PLANT_UPLAND_TERM_ID, 100, TermTermType.LANDCOVER
666
- ),
667
- fake_management(
668
- IRRIGATED_TERM_ID, 100, TermTermType.WATERREGIME
669
- )
670
- ]
671
- result_irrigated = _assign_ipcc_land_use_category(
672
- SITE_TYPE_CROPLAND,
673
- management_nodes_irrigated,
674
- IPCC_SOIL_CATEGORY_LAC
675
- )
676
-
677
- assert result_irrigated == EXPECTED_IRRIGATED
678
-
679
- # Not irrigated upland rice
680
- management_nodes_not_irrigated = [
681
- fake_management(
682
- RICE_PLANT_UPLAND_TERM_ID, 100, TermTermType.LANDCOVER
683
- )
684
- ]
685
- result_not_irrigated = _assign_ipcc_land_use_category(
686
- SITE_TYPE_CROPLAND,
687
- management_nodes_not_irrigated,
688
- IPCC_SOIL_CATEGORY_LAC
689
- )
690
-
691
- assert result_not_irrigated == EXPECTED_NOT_IRRIGATED
692
-
693
-
694
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
695
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
696
- def test_assign_ipcc_land_use_category_no_site_type_case(*args):
697
- EXPECTED = IpccLandUseCategory.OTHER
698
- SITE_TYPE = None
699
- MANAGEMENT_NODES = []
700
-
701
- result = _assign_ipcc_land_use_category(
702
- SITE_TYPE, MANAGEMENT_NODES, IPCC_SOIL_CATEGORY_LAC
703
- )
704
-
705
- assert result == EXPECTED
706
-
707
-
708
- # --- IPCC MANAGEMENT CATEGORY TESTS ---
709
-
710
-
711
- EXPECTED_IPCC_MANAGEMENT_CATEGORY_TO_LAND_COVER_TERM_ID = {
712
- IpccManagementCategory.SEVERELY_DEGRADED: 'severelyDegradedPasture',
713
- IpccManagementCategory.IMPROVED_GRASSLAND: 'improvedPasture',
714
- IpccManagementCategory.HIGH_INTENSITY_GRAZING: 'highIntensityGrazingPasture',
715
- IpccManagementCategory.NOMINALLY_MANAGED: 'nominallyManagedPasture',
716
- IpccManagementCategory.OTHER: 'nativePasture'
717
- }
718
-
719
- EXPECTED_IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_TERM_ID = {
720
- IpccManagementCategory.FULL_TILLAGE: 'fullTillage',
721
- IpccManagementCategory.REDUCED_TILLAGE: 'minimumTillage',
722
- IpccManagementCategory.NO_TILLAGE: 'noTillage'
723
- }
724
-
725
-
726
- def test_assign_ipcc_management_category_standard_cases():
727
-
728
- # Grassland
729
- for expected, term_id in EXPECTED_IPCC_MANAGEMENT_CATEGORY_TO_LAND_COVER_TERM_ID.items():
730
-
731
- management_nodes = [
732
- fake_management(
733
- term_id,
734
- 100,
735
- TermTermType.LANDCOVER
736
- )
737
- ]
738
- result = _assign_ipcc_management_category(
739
- management_nodes,
740
- IpccLandUseCategory.GRASSLAND
741
- )
742
-
743
- assert result == expected
744
-
745
- # Annual crops
746
- for expected, term_id in EXPECTED_IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_TERM_ID.items():
747
-
748
- management_nodes = [
749
- fake_management(term_id, 100, TermTermType.TILLAGE)
750
- ]
751
- result = _assign_ipcc_management_category(
752
- management_nodes,
753
- IpccLandUseCategory.ANNUAL_CROPS
754
- )
755
-
756
- assert result == expected
757
-
758
- # Annual crops wet
759
- for expected, term_id in EXPECTED_IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_TERM_ID.items():
760
-
761
- management_nodes = [
762
- fake_management(term_id, 100, TermTermType.TILLAGE)
763
- ]
764
- result = _assign_ipcc_management_category(
765
- management_nodes,
766
- IpccLandUseCategory.ANNUAL_CROPS_WET
767
- )
768
-
769
- assert result == expected
770
-
771
-
772
- def test_assign_ipcc_management_fractional_cases():
773
- EXPECTED_GRASSLAND = IpccManagementCategory.IMPROVED_GRASSLAND
774
- LAND_COVER_TERM_IDS = [
775
- 'severelyDegradedPasture', # 25%
776
- 'improvedPasture', # 25%
777
- 'improvedPasture', # 25%
778
- 'nativePasture' # 25%
779
- ]
780
-
781
- EXPECTED_CROPLAND = IpccManagementCategory.REDUCED_TILLAGE
782
- TILLAGE_TERM_IDS = [
783
- 'fullTillage', # 25%
784
- 'mulchTillage', # 25%
785
- 'stripTillage', # 25%
786
- 'noTillage' # 25%
787
- ]
788
-
789
- # Grassland
790
- management_nodes_grassland = [
791
- fake_management(
792
- term_id,
793
- 100/len(LAND_COVER_TERM_IDS),
794
- TermTermType.LANDCOVER
795
- )
796
- for term_id in LAND_COVER_TERM_IDS
797
- ]
798
- result_grassland = _assign_ipcc_management_category(
799
- management_nodes_grassland,
800
- IpccLandUseCategory.GRASSLAND
801
- )
802
-
803
- assert result_grassland == EXPECTED_GRASSLAND
804
-
805
- # Annual crops
806
- management_nodes_cropland = [
807
- fake_management(
808
- term_id,
809
- 100/len(TILLAGE_TERM_IDS),
810
- TermTermType.TILLAGE
811
- )
812
- for term_id in TILLAGE_TERM_IDS
813
- ]
814
- result_cropland = _assign_ipcc_management_category(
815
- management_nodes_cropland,
816
- IpccLandUseCategory.ANNUAL_CROPS
817
- )
818
-
819
- assert result_cropland == EXPECTED_CROPLAND
820
-
821
-
822
- def test_assign_ipcc_management_category_no_management_cases():
823
-
824
- EXPECTED_GRASSLAND = IpccManagementCategory.NOMINALLY_MANAGED
825
- EXPECTED_CROPLAND = IpccManagementCategory.FULL_TILLAGE
826
- MANAGEMENT_NODES = []
827
-
828
- result_grassland = _assign_ipcc_management_category(
829
- MANAGEMENT_NODES, IpccLandUseCategory.GRASSLAND
830
- )
831
-
832
- assert result_grassland == EXPECTED_GRASSLAND
833
-
834
- result_cropland = _assign_ipcc_management_category(
835
- MANAGEMENT_NODES, IpccLandUseCategory.ANNUAL_CROPS
836
- )
837
-
838
- assert result_cropland == EXPECTED_CROPLAND
839
-
840
-
841
335
  # --- IPCC CARBON INPUT CATEGORY TESTS ---
842
336
 
843
- ANIMAL_MANURE_USED_NODE = fake_management(
844
- 'animalManureUsed', True, TermTermType.LANDUSEMANAGEMENT
845
- )
846
-
847
- BROCCOLI_PLANT_NODE = fake_management(
848
- 'broccoliPlant', 100, TermTermType.LANDCOVER
849
- )
850
-
851
- COMMON_BEAN_PLANT_NODE = fake_management(
852
- 'commonBeanPlant', 100, TermTermType.LANDCOVER
853
- )
854
-
855
- CLOVER_PLANT_NODE = fake_management(
856
- 'cloverPlant', 100, TermTermType.LANDCOVER
857
- )
858
-
859
- IRRIGATED_NODE = fake_management(
860
- 'irrigated', 100, TermTermType.WATERREGIME
861
- )
862
-
863
- INORGANIC_NITROGEN_FERTILISER_USED_NODE = fake_management(
864
- 'inorganicNitrogenFertiliserUsed', True, TermTermType.LANDUSEMANAGEMENT
865
- )
866
-
867
- MULCHING_NODE = fake_management(
868
- 'mulching', 100, TermTermType.LANDUSEMANAGEMENT
869
- )
870
-
871
- ORGANIC_FERTILISER_USED_NODE = fake_management(
872
- 'organicFertiliserOrSoilCarbonIncreasingAmendmentUsed',
873
- 100,
874
- TermTermType.LANDUSEMANAGEMENT
875
- )
876
-
877
- RESIDUE_REMOVED_NODE = fake_management(
878
- 'residueRemoved', 100, TermTermType.CROPRESIDUEMANAGEMENT
879
- )
880
-
881
- SHORT_BARE_FALLOW_NODE = fake_management(
882
- 'shortBareFallow', 100, TermTermType.LANDUSEMANAGEMENT
883
- )
884
-
885
- RYEGRASS_PLANT_AS_COVER_CROP_NODE = fake_management(
886
- 'ryegrassPlant', 100, TermTermType.LANDCOVER,
887
- properties=[
888
- fake_property(
889
- 'coverCrop',
890
- True
891
- )
892
- ]
893
- )
894
-
895
- WHEAT_PLANT_NODE = fake_management(
896
- 'wheatPlant', 100, TermTermType.LANDCOVER
897
- )
898
-
899
-
900
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
901
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
902
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
903
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
904
- def test_check_cropland_high_with_manure_category(*args):
905
- IPCC_CARBON_INPUT_CATEGORY = IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE
906
- EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES = {
907
- 1: [
908
- COMMON_BEAN_PLANT_NODE, # N-fixing crop
909
- ANIMAL_MANURE_USED_NODE
910
- ]
911
- }
912
-
913
- for expected, management_nodes in EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES.items():
914
- carbon_input_args = _make_carbon_input_args(management_nodes)
915
- result = _check_cropland_high_with_manure_category(
916
- carbon_input_args,
917
- key=IPCC_CARBON_INPUT_CATEGORY
918
- )
919
-
920
- assert result == expected
921
- pass
922
-
923
337
 
338
+ @mark.parametrize("key", [1, 2, 3, 4], ids=lambda key: f"scenario-{key}")
924
339
  @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
925
340
  @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
926
341
  @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
342
+ @patch(f"{class_path}.get_long_fallow_land_cover_terms", return_value=LONG_FALLOW_LAND_COVER_TERM_IDS)
927
343
  @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
928
- def test_check_cropland_high_without_manure_category(*args):
929
- IPCC_CARBON_INPUT_CATEGORY = IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE
930
- EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES = {
931
- 1: [
932
- WHEAT_PLANT_NODE, # Non-N-fixing & high-residue-producing crop
933
- INORGANIC_NITROGEN_FERTILISER_USED_NODE,
934
- ORGANIC_FERTILISER_USED_NODE
935
- ]
936
- }
937
-
938
- for expected, management_nodes in EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES.items():
939
- carbon_input_args = _make_carbon_input_args(management_nodes)
940
- result = _check_cropland_high_without_manure_category(
941
- carbon_input_args,
942
- key=IPCC_CARBON_INPUT_CATEGORY
943
- )
944
-
945
- assert result == expected
946
-
947
-
948
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
949
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
950
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
951
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
952
- def test_check_cropland_medium_category(*args):
953
- IPCC_CARBON_INPUT_CATEGORY = IpccCarbonInputCategory.CROPLAND_MEDIUM
954
- EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES = {
955
- 1: [
956
- WHEAT_PLANT_NODE, # Non-N-fixing & high-residue-producing crop
957
- RESIDUE_REMOVED_NODE,
958
- ANIMAL_MANURE_USED_NODE
959
- ],
960
- 2: [
961
- BROCCOLI_PLANT_NODE, # Low-residue-producing crop
962
- IRRIGATED_NODE
963
- ],
964
- 3: [
965
- WHEAT_PLANT_NODE, # Non-N-fixing & high-residue-producing crop
966
- RYEGRASS_PLANT_AS_COVER_CROP_NODE # Practice increasing C input
967
- ],
968
- 4: [
969
- COMMON_BEAN_PLANT_NODE # N-fixing crop
970
- ]
971
- }
972
-
973
- for expected, management_nodes in EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES.items():
974
- carbon_input_args = _make_carbon_input_args(management_nodes)
975
- result = _check_cropland_medium_category(
976
- carbon_input_args,
977
- key=IPCC_CARBON_INPUT_CATEGORY
978
- )
979
-
980
- assert result == expected
981
-
982
-
983
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
984
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
985
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
986
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
987
- def test_check_cropland_low_category(*args):
988
- IPCC_CARBON_INPUT_CATEGORY = IpccCarbonInputCategory.CROPLAND_LOW
989
- EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES = {
990
- 1: [
991
- WHEAT_PLANT_NODE, # Non-N-fixing & high-residue-producing crop
992
- RESIDUE_REMOVED_NODE
993
- ],
994
- 2: [
995
- WHEAT_PLANT_NODE, # Low-residue-producing crop
996
- SHORT_BARE_FALLOW_NODE
997
- ],
998
- 3: [],
999
- }
1000
-
1001
- for expected, management_nodes in EXPECTED_CONDITION_KEY_TO_MANAGEMENT_NODES.items():
1002
- carbon_input_args = _make_carbon_input_args(management_nodes)
1003
- result = _check_cropland_low_category(
1004
- carbon_input_args,
1005
- key=IPCC_CARBON_INPUT_CATEGORY
1006
- )
1007
- assert result == expected
1008
-
1009
-
1010
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1011
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1012
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
1013
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1014
- def test_assign_ipcc_carbon_input_category_grassland_cases(*args):
1015
- GRASSLAND_CARBON_INPUT_CATEGORY_MANAGEMENT_FIXTURES_GROUPED = {
1016
- IpccCarbonInputCategory.GRASSLAND_HIGH: [
1017
- [
1018
- IRRIGATED_NODE,
1019
- INORGANIC_NITROGEN_FERTILISER_USED_NODE
1020
- ],
1021
- [
1022
- MULCHING_NODE,
1023
- CLOVER_PLANT_NODE
1024
- ]
1025
- ],
1026
- IpccCarbonInputCategory.GRASSLAND_MEDIUM: [
1027
- [
1028
- ORGANIC_FERTILISER_USED_NODE
1029
- ],
1030
- [
1031
- CLOVER_PLANT_NODE
1032
- ]
1033
- ]
1034
- }
1035
-
1036
- for expected, management_node_groups in GRASSLAND_CARBON_INPUT_CATEGORY_MANAGEMENT_FIXTURES_GROUPED.items():
1037
- for management_nodes in management_node_groups:
1038
-
1039
- result = _assign_ipcc_carbon_input_category(
1040
- management_nodes,
1041
- IpccManagementCategory.IMPROVED_GRASSLAND
1042
- )
1043
-
1044
- assert result == expected
1045
-
1046
-
1047
- # --- TIER 1 TESTS ---
1048
-
1049
-
1050
- def test_run_tier_1_soc_stocks():
1051
- IPCC_TIMESTAMPS = [1990, 1995, 2000, 2005, 2010, 2015, 2020]
1052
-
1053
- IPCC_TIER_1_SOC_STOCK_FIXTURES = [
1054
- # Land unit 1
1055
- Tier1SocStockFixtures(
1056
- timestamps=IPCC_TIMESTAMPS,
1057
- soc_equilibriums=[
1058
- 77.000, 70.840, 70.840, 70.840, 70.840, 70.840, 70.840
1059
- ],
1060
- expected=[
1061
- 77.000, 75.460, 73.920, 72.380, 70.840, 70.840, 70.840
1062
- ]
1063
- ),
1064
- # Land unit 2
1065
- Tier1SocStockFixtures(
1066
- timestamps=IPCC_TIMESTAMPS,
1067
- soc_equilibriums=[
1068
- 77.000, 70.840, 70.840, 70.840, 80.850, 80.850, 80.850
1069
- ],
1070
- expected=[
1071
- 77.000, 75.460, 73.920, 72.380, 74.498, 76.615, 78.733
1072
- ]
1073
- ),
1074
- # Land unit 3
1075
- Tier1SocStockFixtures(
1076
- timestamps=IPCC_TIMESTAMPS,
1077
- soc_equilibriums=[
1078
- 80.850, 70.840, 70.840, 70.840, 70.840, 80.850, 80.850
1079
- ],
1080
- expected=[
1081
- 80.850, 78.348, 75.845, 73.343, 70.840, 73.343, 75.845
1082
- ]
1083
- ),
1084
- # Land unit 4
1085
- Tier1SocStockFixtures(
1086
- timestamps=IPCC_TIMESTAMPS,
1087
- soc_equilibriums=[
1088
- 80.850, 80.850, 77.000, 77.000, 77.000, 77.000, 77.000
1089
- ],
1090
- expected=[
1091
- 80.850, 80.850, 79.888, 78.925, 77.963, 77.000, 77.000
1092
- ]
1093
- ),
1094
- # Land unit 5
1095
- Tier1SocStockFixtures(
1096
- timestamps=IPCC_TIMESTAMPS,
1097
- soc_equilibriums=[
1098
- 70.840, 70.840, 70.840, 70.840, 80.850, 80.850, 80.850
1099
- ],
1100
- expected=[
1101
- 70.840, 70.840, 70.840, 70.840, 73.343, 75.845, 78.348
1102
- ]
1103
- ),
1104
- # Land unit 6
1105
- Tier1SocStockFixtures(
1106
- timestamps=IPCC_TIMESTAMPS,
1107
- soc_equilibriums=[
1108
- 70.840, 70.840, 80.850, 80.850, 80.850, 70.840, 80.850
1109
- ],
1110
- expected=[
1111
- 70.840, 70.840, 73.343, 75.845, 78.348, 76.471, 77.565
1112
- ]
1113
- )
1114
- ]
1115
-
1116
- for fixture in IPCC_TIER_1_SOC_STOCK_FIXTURES:
1117
- result = _calc_tier_1_soc_stocks(
1118
- fixture.timestamps, fixture.soc_equilibriums
1119
- )
1120
- assert_almost_equal(result, fixture.expected, decimal=3)
1121
-
1122
-
1123
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
1124
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1125
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1126
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
1127
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1128
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
1129
- @patch(f"{class_path}.related_cycles", return_value=[])
1130
- def test_run_tier_1_cropland_with_measured_soc(*args):
1131
- SUBFOLDER = TIER_1_SUBFOLDERS[0]
1132
- folder = f"{fixtures_folder}/{SUBFOLDER}"
344
+ def test_check_cropland_medium_category(
345
+ _mock_get_residue_removed_or_burnt_terms,
346
+ _mock_get_long_fallow_land_cover_terms,
347
+ _mock_get_irrigated_terms,
348
+ _mock_get_crop_residue_incorporated_or_left_on_field_terms,
349
+ _mock_get_cover_crop_property_terms,
350
+ key
351
+ ):
352
+ """
353
+ Tests each set of cropland medium conditions against a list of nodes that such satisfy it. The function returns the
354
+ key of the matching condition set, which should match the suffix of the fixtures subfolder.
355
+ """
356
+ folder = f"{fixtures_folder}/IpccCarbonInputCategory/cropland-medium/scenario-{key}"
1133
357
 
1134
358
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
1135
359
  site = json.load(f)
1136
360
 
1137
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
1138
- expected = json.load(f)
1139
-
1140
- result = run(site)
1141
- assert result == expected
361
+ result = _check_cropland_medium_category(**_get_carbon_input_kwargs(site.get("management", [])))
362
+ assert result == key
1142
363
 
1143
364
 
1144
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
365
+ @mark.parametrize("key", [1, 2, 3], ids=lambda key: f"scenario-{key}")
1145
366
  @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1146
367
  @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1147
368
  @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
369
+ @patch(f"{class_path}.get_long_fallow_land_cover_terms", return_value=LONG_FALLOW_LAND_COVER_TERM_IDS)
1148
370
  @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1149
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
1150
- @patch(f"{class_path}.related_cycles", return_value=[])
1151
- def test_run_tier_1_cropland_without_measured_soc(*args):
1152
-
1153
- SUBFOLDER = TIER_1_SUBFOLDERS[1]
1154
- folder = f"{fixtures_folder}/{SUBFOLDER}"
371
+ def test_check_cropland_low_category(
372
+ _mock_get_residue_removed_or_burnt_terms,
373
+ _mock_get_long_fallow_land_cover_terms,
374
+ _mock_get_irrigated_terms,
375
+ _mock_get_crop_residue_incorporated_or_left_on_field_terms,
376
+ _mock_get_cover_crop_property_terms,
377
+ key
378
+ ):
379
+ """
380
+ Tests each set of cropland low conditions against a list of nodes that such satisfy it. The function returns the
381
+ key of the matching condition set, which should match the suffix of the fixtures subfolder.
382
+ """
383
+ folder = f"{fixtures_folder}/IpccCarbonInputCategory/cropland-low/scenario-{key}"
1155
384
 
1156
385
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
1157
386
  site = json.load(f)
1158
387
 
1159
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
1160
- expected = json.load(f)
1161
-
1162
- result = run(site)
1163
- assert result == expected
388
+ result = _check_cropland_low_category(**_get_carbon_input_kwargs(site.get("management", [])))
389
+ assert result == key
390
+
391
+
392
+ # subfolder, management_category, expected
393
+ CARBON_INPUT_CATEGORY_PARAMS = [
394
+ (
395
+ "cropland-high-with-manure",
396
+ IpccManagementCategory.FULL_TILLAGE,
397
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE
398
+ ),
399
+ (
400
+ "cropland-high-without-manure",
401
+ IpccManagementCategory.FULL_TILLAGE,
402
+ IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE
403
+ ),
404
+ ("cropland-low/scenario-1", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_LOW),
405
+ ("cropland-low/scenario-2", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_LOW),
406
+ ("cropland-low/scenario-3", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_LOW),
407
+ ("cropland-medium/scenario-1", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
408
+ ("cropland-medium/scenario-2", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
409
+ ("cropland-medium/scenario-3", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
410
+ ("cropland-medium/scenario-4", IpccManagementCategory.FULL_TILLAGE, IpccCarbonInputCategory.CROPLAND_MEDIUM),
411
+ ("grassland-high", IpccManagementCategory.IMPROVED_GRASSLAND, IpccCarbonInputCategory.GRASSLAND_HIGH),
412
+ (
413
+ "grassland-medium/0-improvements",
414
+ IpccManagementCategory.IMPROVED_GRASSLAND,
415
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM
416
+ ),
417
+ (
418
+ "grassland-medium/1-improvements",
419
+ IpccManagementCategory.IMPROVED_GRASSLAND,
420
+ IpccCarbonInputCategory.GRASSLAND_MEDIUM
421
+ )
422
+ ]
1164
423
 
1165
424
 
1166
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
425
+ @mark.parametrize(
426
+ "subfolder, management_category, expected",
427
+ CARBON_INPUT_CATEGORY_PARAMS,
428
+ ids=[params[0] for params in CARBON_INPUT_CATEGORY_PARAMS]
429
+ )
1167
430
  @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1168
431
  @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1169
432
  @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
433
+ @patch(f"{class_path}.get_long_fallow_land_cover_terms", return_value=LONG_FALLOW_LAND_COVER_TERM_IDS)
1170
434
  @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1171
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
1172
- @patch(f"{class_path}.related_cycles", return_value=[])
1173
- def test_run_tier_1_permanent_pasture(*args):
1174
-
1175
- SUBFOLDER = TIER_1_SUBFOLDERS[2]
1176
- folder = f"{fixtures_folder}/{SUBFOLDER}"
435
+ def test_assign_ipcc_carbon_input_category(
436
+ _mock_get_residue_removed_or_burnt_terms,
437
+ _mock_get_long_fallow_land_cover_terms,
438
+ _mock_get_irrigated_terms,
439
+ _mock_get_crop_residue_incorporated_or_left_on_field_terms,
440
+ _mock_get_cover_crop_property_terms,
441
+ subfolder,
442
+ management_category,
443
+ expected
444
+ ):
445
+ folder = f"{fixtures_folder}/IpccCarbonInputCategory/{subfolder}"
1177
446
 
1178
447
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
1179
448
  site = json.load(f)
1180
449
 
1181
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
1182
- expected = json.load(f)
1183
-
1184
- result = run(site)
450
+ result = _assign_ipcc_carbon_input_category(site.get("management", []), management_category)
1185
451
  assert result == expected
1186
452
 
1187
453
 
1188
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
1189
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1190
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1191
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
1192
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1193
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
1194
- @patch(f"{class_path}.related_cycles", return_value=[])
1195
- def test_run_tier_1_should_not_run(*args):
1196
-
1197
- SUBFOLDER = TIER_1_SUBFOLDERS[3]
1198
- folder = f"{fixtures_folder}/{SUBFOLDER}"
1199
-
1200
- with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
1201
- site = json.load(f)
1202
-
1203
- result = run(site)
1204
- assert result == []
454
+ # --- TIER 1 TESTS ---
1205
455
 
1206
456
 
1207
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
1208
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1209
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1210
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
1211
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1212
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
1213
- @patch(f"{class_path}.related_cycles", return_value=[])
1214
- def test_run_tier_1_without_management_with_measured_soc(*args):
457
+ @mark.parametrize(
458
+ "soc_equilibriums, expected",
459
+ [
460
+ (
461
+ [77.000, 70.840, 70.840, 70.840, 70.840, 70.840, 70.840],
462
+ [77.000, 75.460, 73.920, 72.380, 70.840, 70.840, 70.840]
463
+ ),
464
+ (
465
+ [77.000, 70.840, 70.840, 70.840, 80.850, 80.850, 80.850],
466
+ [77.000, 75.460, 73.920, 72.380, 74.498, 76.615, 78.733]
467
+ ),
468
+ (
469
+ [80.850, 70.840, 70.840, 70.840, 70.840, 80.850, 80.850],
470
+ [80.850, 78.348, 75.845, 73.343, 70.840, 73.343, 75.845]
471
+ ),
472
+ (
473
+ [80.850, 80.850, 77.000, 77.000, 77.000, 77.000, 77.000],
474
+ [80.850, 80.850, 79.888, 78.925, 77.963, 77.000, 77.000]
475
+ ),
476
+ (
477
+ [70.840, 70.840, 70.840, 70.840, 80.850, 80.850, 80.850],
478
+ [70.840, 70.840, 70.840, 70.840, 73.343, 75.845, 78.348]
479
+ ),
480
+ (
481
+ [70.840, 70.840, 80.850, 80.850, 80.850, 70.840, 80.850],
482
+ [70.840, 70.840, 73.343, 75.845, 78.348, 76.471, 77.565]
483
+ ),
484
+ ],
485
+ ids=["land-unit-1", "land-unit-2", "land-unit-3", "land-unit-4", "land-unit-5", "land-unit-6"]
486
+ )
487
+ def test_run_tier_1_soc_stocks(soc_equilibriums, expected):
1215
488
  """
1216
- This is to test for Sites with measured SOC stock values but no management nodes.
1217
- The model should not run but should not raise errors either.
489
+ Test the interpolation between SOC equilibriums using test data provided in IPCC (2019).
1218
490
  """
491
+ TIMESTAMPS = [1990, 1995, 2000, 2005, 2010, 2015, 2020]
492
+ result = _calc_tier_1_soc_stocks(
493
+ TIMESTAMPS, soc_equilibriums
494
+ )
495
+ assert_almost_equal(result, expected, decimal=3)
1219
496
 
1220
- SUBFOLDER = TIER_1_SUBFOLDERS[4]
1221
- folder = f"{fixtures_folder}/{SUBFOLDER}"
1222
-
1223
- with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
1224
- site = json.load(f)
1225
-
1226
- result = run(site)
1227
- assert result == []
1228
497
 
498
+ def test_iterate_soc_equilibriums():
499
+ TIMESTAMPS = [1990, 2020]
500
+ SOC_EQUILIBRIUMS = [20000, 40000]
1229
501
 
1230
- @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
1231
- @patch(f"{class_path}.get_cover_crop_property_terms", return_value=COVER_CROP_PROPERTY_TERM_IDS)
1232
- @patch(f"{class_path}.get_crop_residue_incorporated_or_left_on_field_terms", return_value=CROP_RESIDUE_INCORP_TERM_IDS)
1233
- @patch(f"{class_path}.get_irrigated_terms", return_value=IRRIGATED_TERM_IDS)
1234
- @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
1235
- @patch(f"{class_path}.get_rice_plant_upland_terms", return_value=RICE_PLANT_UPLAND_TERM_IDS)
1236
- @patch(f"{class_path}.related_cycles", return_value=[])
1237
- def test_run_tier_1_cropland_depth_as_float(*args):
1238
- SUBFOLDER = TIER_1_SUBFOLDERS[5]
1239
- folder = f"{fixtures_folder}/{SUBFOLDER}"
1240
-
1241
- with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
1242
- site = json.load(f)
1243
-
1244
- with open(f"{folder}/result.jsonld", encoding='utf-8') as f:
1245
- expected = json.load(f)
502
+ EXPECTED = (
503
+ [1990, 2010, 2020],
504
+ [20000, 40000, 40000]
505
+ )
1246
506
 
1247
- result = run(site)
1248
- print(json.dumps(result, indent=2))
1249
- assert result == expected
507
+ result = _iterate_soc_equilibriums(TIMESTAMPS, SOC_EQUILIBRIUMS)
508
+ assert result == EXPECTED