hestia-earth-models 0.57.2__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 (109) hide show
  1. hestia_earth/models/cycle/aboveGroundCropResidueTotal.py +17 -12
  2. hestia_earth/models/cycle/excretaKgMass.py +4 -5
  3. hestia_earth/models/cycle/excretaKgN.py +4 -5
  4. hestia_earth/models/cycle/excretaKgVs.py +4 -5
  5. hestia_earth/models/cycle/inorganicFertiliser.py +2 -2
  6. hestia_earth/models/cycle/{irrigated.py → irrigatedTypeUnspecified.py} +4 -4
  7. hestia_earth/models/cycle/liveAnimal.py +9 -11
  8. hestia_earth/models/cycle/milkYield.py +154 -0
  9. hestia_earth/models/cycle/residueIncorporated.py +1 -1
  10. hestia_earth/models/cycle/utils.py +6 -0
  11. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +3 -3
  12. hestia_earth/models/faostat2018/seed.py +2 -3
  13. hestia_earth/models/geospatialDatabase/clayContent.py +17 -4
  14. hestia_earth/models/geospatialDatabase/sandContent.py +17 -4
  15. hestia_earth/models/geospatialDatabase/siltContent.py +2 -2
  16. hestia_earth/models/impact_assessment/irrigated.py +0 -3
  17. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +2 -2
  18. hestia_earth/models/ipcc2006/n2OToAirCropResidueDecompositionIndirect.py +2 -2
  19. hestia_earth/models/ipcc2006/n2OToAirExcretaDirect.py +1 -1
  20. hestia_earth/models/ipcc2006/n2OToAirExcretaIndirect.py +8 -4
  21. hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserDirect.py +4 -1
  22. hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserIndirect.py +1 -1
  23. hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserDirect.py +1 -1
  24. hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserIndirect.py +1 -1
  25. hestia_earth/models/ipcc2006/utils.py +11 -8
  26. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +4 -4
  27. hestia_earth/models/ipcc2019/ch4ToAirFloodedRice.py +16 -7
  28. hestia_earth/models/ipcc2019/co2ToAirSoilCarbonStockChangeManagementChange.py +759 -0
  29. hestia_earth/models/ipcc2019/croppingDuration.py +12 -6
  30. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +5 -52
  31. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserDirect.py +104 -0
  32. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +1 -1
  33. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserDirect.py +105 -0
  34. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +1 -1
  35. hestia_earth/models/ipcc2019/no3ToGroundwaterCropResidueDecomposition.py +1 -1
  36. hestia_earth/models/ipcc2019/no3ToGroundwaterExcreta.py +1 -1
  37. hestia_earth/models/ipcc2019/no3ToGroundwaterInorganicFertiliser.py +1 -1
  38. hestia_earth/models/ipcc2019/no3ToGroundwaterOrganicFertiliser.py +1 -1
  39. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +1088 -1268
  40. hestia_earth/models/ipcc2019/pastureGrass.py +4 -4
  41. hestia_earth/models/ipcc2019/utils.py +102 -1
  42. hestia_earth/models/koble2014/aboveGroundCropResidue.py +15 -17
  43. hestia_earth/models/koble2014/cropResidueManagement.py +2 -2
  44. hestia_earth/models/koble2014/utils.py +19 -3
  45. hestia_earth/models/linkedImpactAssessment/__init__.py +4 -2
  46. hestia_earth/models/log.py +15 -3
  47. hestia_earth/models/mocking/search-results.json +184 -118
  48. hestia_earth/models/pooreNemecek2018/excretaKgN.py +6 -7
  49. hestia_earth/models/pooreNemecek2018/excretaKgVs.py +7 -6
  50. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterCropResidueDecomposition.py +3 -2
  51. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterExcreta.py +3 -2
  52. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterInorganicFertiliser.py +3 -2
  53. hestia_earth/models/pooreNemecek2018/saplings.py +0 -1
  54. hestia_earth/models/site/management.py +168 -0
  55. hestia_earth/models/site/organicCarbonPerHa.py +251 -89
  56. hestia_earth/models/stehfestBouwman2006/n2OToAirCropResidueDecompositionDirect.py +3 -2
  57. hestia_earth/models/stehfestBouwman2006/n2OToAirExcretaDirect.py +3 -2
  58. hestia_earth/models/stehfestBouwman2006/n2OToAirInorganicFertiliserDirect.py +3 -2
  59. hestia_earth/models/stehfestBouwman2006/n2OToAirOrganicFertiliserDirect.py +3 -2
  60. hestia_earth/models/stehfestBouwman2006/noxToAirCropResidueDecomposition.py +3 -2
  61. hestia_earth/models/stehfestBouwman2006/noxToAirExcreta.py +3 -2
  62. hestia_earth/models/stehfestBouwman2006/noxToAirInorganicFertiliser.py +3 -2
  63. hestia_earth/models/stehfestBouwman2006/noxToAirOrganicFertiliser.py +3 -2
  64. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirCropResidueDecomposition.py +3 -2
  65. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirExcreta.py +3 -2
  66. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirInorganicFertiliser.py +3 -2
  67. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirOrganicFertiliser.py +3 -2
  68. hestia_earth/models/utils/aggregated.py +1 -0
  69. hestia_earth/models/utils/blank_node.py +394 -72
  70. hestia_earth/models/utils/cropResidue.py +13 -0
  71. hestia_earth/models/utils/cycle.py +18 -9
  72. hestia_earth/models/utils/measurement.py +1 -1
  73. hestia_earth/models/utils/property.py +4 -4
  74. hestia_earth/models/utils/term.py +48 -3
  75. hestia_earth/models/version.py +1 -1
  76. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/METADATA +5 -9
  77. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/RECORD +109 -97
  78. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/WHEEL +1 -1
  79. tests/models/cycle/animal/input/test_hestiaAggregatedData.py +2 -14
  80. tests/models/cycle/input/test_hestiaAggregatedData.py +4 -16
  81. tests/models/cycle/test_coldCarcassWeightPerHead.py +1 -1
  82. tests/models/cycle/test_coldDressedCarcassWeightPerHead.py +1 -1
  83. tests/models/cycle/{test_irrigated.py → test_irrigatedTypeUnspecified.py} +1 -1
  84. tests/models/cycle/test_milkYield.py +58 -0
  85. tests/models/cycle/test_readyToCookWeightPerHead.py +1 -1
  86. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +1 -1
  87. tests/models/geospatialDatabase/test_clayContent.py +9 -3
  88. tests/models/geospatialDatabase/test_sandContent.py +9 -3
  89. tests/models/ipcc2006/test_n2OToAirExcretaDirect.py +7 -2
  90. tests/models/ipcc2006/test_n2OToAirExcretaIndirect.py +1 -1
  91. tests/models/ipcc2006/test_n2OToAirInorganicFertiliserDirect.py +7 -2
  92. tests/models/ipcc2006/test_n2OToAirInorganicFertiliserIndirect.py +7 -2
  93. tests/models/ipcc2006/test_n2OToAirOrganicFertiliserDirect.py +7 -2
  94. tests/models/ipcc2006/test_n2OToAirOrganicFertiliserIndirect.py +7 -2
  95. tests/models/ipcc2019/test_ch4ToAirEntericFermentation.py +1 -1
  96. tests/models/ipcc2019/test_co2ToAirSoilCarbonStockChangeManagementChange.py +228 -0
  97. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserDirect.py +74 -0
  98. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserDirect.py +74 -0
  99. tests/models/ipcc2019/test_organicCarbonPerHa.py +303 -1044
  100. tests/models/koble2014/test_residueBurnt.py +1 -2
  101. tests/models/koble2014/test_residueLeftOnField.py +1 -2
  102. tests/models/koble2014/test_residueRemoved.py +1 -2
  103. tests/models/koble2014/test_utils.py +52 -0
  104. tests/models/site/test_management.py +117 -0
  105. tests/models/site/test_organicCarbonPerHa.py +51 -5
  106. tests/models/utils/test_blank_node.py +230 -34
  107. tests/models/utils/test_term.py +17 -3
  108. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/LICENSE +0 -0
  109. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/top_level.txt +0 -0
@@ -4,13 +4,8 @@ changes. This model combines the Tier 1 & Tier 2 methodologies. It first tries t
4
4
  remaining croplands). If Tier 2 cannot run, it will try to run Tier 1 (for croplands remaining croplands and for
5
5
  grasslands remaining grasslands). Source:
6
6
  [IPCC 2019, Vol. 4, Chapter 10](https://www.ipcc-nggip.iges.or.jp/public/2019rf/pdf/4_Volume4/19R_V4_Ch05_Cropland.pdf).
7
-
8
- Currently, the Tier 2 implementation does not take into account the irrigation of cycles when estimating soil organic
9
- carbon stock changes.
10
7
  """
11
- from collections.abc import Iterable
12
8
  from enum import Enum
13
- from functools import reduce
14
9
  from numpy import exp
15
10
  from pydash.objects import merge
16
11
  from statistics import mean
@@ -21,21 +16,23 @@ from typing import (
21
16
  Union
22
17
  )
23
18
  from hestia_earth.schema import (
19
+ CycleFunctionalUnit,
24
20
  MeasurementMethodClassification,
25
21
  SiteSiteType,
26
- TermTermType
22
+ TermTermType,
27
23
  )
28
- from hestia_earth.utils.date import diff_in_years
29
24
  from hestia_earth.utils.model import find_term_match, filter_list_term_type
30
- from hestia_earth.utils.tools import flatten, list_sum, non_empty_list, safe_parse_date
25
+ from hestia_earth.utils.tools import flatten, list_sum, non_empty_list
31
26
 
32
- from hestia_earth.models.log import logShouldRun, log_as_table
27
+ from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
33
28
  from hestia_earth.models.utils.blank_node import (
34
29
  cumulative_nodes_match,
35
30
  cumulative_nodes_lookup_match,
36
31
  cumulative_nodes_term_match,
37
32
  get_node_value,
33
+ group_nodes_by_year_and_month,
38
34
  group_nodes_by_year,
35
+ GroupNodesByYearMode,
39
36
  node_lookup_match,
40
37
  node_term_match
41
38
  )
@@ -43,8 +40,6 @@ from hestia_earth.models.utils.cycle import check_cycle_site_ids_identical
43
40
  from hestia_earth.models.utils.ecoClimateZone import get_ecoClimateZone_lookup_value
44
41
  from hestia_earth.models.utils.measurement import (
45
42
  _new_measurement,
46
- group_measurement_values_by_year,
47
- most_relevant_measurement_value_by_depth_and_date
48
43
  )
49
44
  from hestia_earth.models.utils.property import get_node_property
50
45
  from hestia_earth.models.utils.site import related_cycles
@@ -52,10 +47,13 @@ from hestia_earth.models.utils.term import (
52
47
  get_cover_crop_property_terms,
53
48
  get_crop_residue_incorporated_or_left_on_field_terms,
54
49
  get_irrigated_terms,
50
+ get_long_fallow_land_cover_terms,
55
51
  get_residue_removed_or_burnt_terms,
56
- get_rice_plant_upland_terms
52
+ get_upland_rice_crop_terms,
53
+ get_upland_rice_land_cover_terms
57
54
  )
58
55
 
56
+ from .utils import check_consecutive
59
57
  from . import MODEL
60
58
 
61
59
  REQUIREMENTS = {
@@ -115,6 +113,7 @@ REQUIREMENTS = {
115
113
  }
116
114
  }
117
115
  LOOKUPS = {
116
+ "crop": "IPCC_LAND_USE_CATEGORY",
118
117
  "ecoClimateZone": [
119
118
  "IPCC_2019_SOC_REF_KG_C_HECTARE_SAN",
120
119
  "IPCC_2019_SOC_REF_KG_C_HECTARE_WET",
@@ -168,6 +167,7 @@ TERM_ID = 'organicCarbonPerHa'
168
167
 
169
168
  MIN_AREA_THRESHOLD = 30 # 30% as per IPCC guidelines
170
169
  SUPER_MAJORITY_AREA_THRESHOLD = 100 - MIN_AREA_THRESHOLD
170
+ MIN_YIELD_THRESHOLD = 1
171
171
  DEPTH_UPPER = 0
172
172
  DEPTH_LOWER = 30
173
173
 
@@ -221,6 +221,14 @@ DEFAULT_PARAMS = {
221
221
  "default_lignin_content": 0.073
222
222
  }
223
223
 
224
+ VALID_SITE_TYPES_TIER_2 = [
225
+ SiteSiteType.CROPLAND.value
226
+ ]
227
+
228
+ VALID_FUNCTIONAL_UNITS_TIER_2 = [
229
+ CycleFunctionalUnit._1_HA.value
230
+ ]
231
+
224
232
  # --- TIER 1 CONSTANTS ---
225
233
 
226
234
  CLAY_CONTENT_TERM_ID = "clayContent"
@@ -231,8 +239,6 @@ ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
231
239
  INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
232
240
  ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserOrSoilCarbonIncreasingAmendmentUsed"
233
241
 
234
- DEFAULT_NODE_VALUE = 100
235
-
236
242
  CLAY_CONTENT_MAX = 8
237
243
  SAND_CONTENT_MIN = 70
238
244
 
@@ -242,6 +248,13 @@ The number of years required for soil organic carbon to reach equilibrium after
242
248
  a change in land use, management regime or carbon input regime.
243
249
  """
244
250
 
251
+ VALID_SITE_TYPES_TIER_1 = [
252
+ SiteSiteType.CROPLAND.value,
253
+ SiteSiteType.FOREST.value,
254
+ SiteSiteType.OTHER_NATURAL_VEGETATION.value,
255
+ SiteSiteType.PERMANENT_PASTURE.value,
256
+ ]
257
+
245
258
  # --- SHARED TIER 1 & TIER 2 FORMAT MEASUREMENT OUTPUT ---
246
259
 
247
260
 
@@ -274,112 +287,115 @@ def _measurement(year: int, value: float, method_classification: str) -> dict:
274
287
 
275
288
  # --- SHARED TIER 1 & TIER 2 ENUMS ---
276
289
 
277
-
278
- IpccManagementCategory = Enum("IpccManagementCategory", [
279
- "SEVERELY_DEGRADED",
280
- "IMPROVED_GRASSLAND",
281
- "HIGH_INTENSITY_GRAZING",
282
- "NOMINALLY_MANAGED",
283
- "FULL_TILLAGE",
284
- "REDUCED_TILLAGE",
285
- "NO_TILLAGE",
286
- "OTHER"
287
- ])
288
- """
289
- Enum representing IPCC Management Categories for grasslands and annual
290
- croplands.
291
-
292
- See [IPCC (2019) Vol. 4, Ch. 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
293
- """
294
-
295
-
296
- # --- TIER 2 ENUMS ---
297
-
298
-
299
- _InnerKey = Enum(
300
- "_InnerKey",
301
- [
302
- "TEMPERATURES",
303
- "PRECIPITATIONS",
304
- "PETS",
305
- "IS_IRRIGATEDS",
306
- "CARBON_SOURCES",
307
- "TILLAGE_CATEGORY"
308
- ],
309
- )
310
-
311
-
312
- INNER_KEYS_RUN_WITH_IRRIGATION = [
313
- _InnerKey.TEMPERATURES,
314
- _InnerKey.PRECIPITATIONS,
315
- _InnerKey.PETS,
316
- _InnerKey.IS_IRRIGATEDS,
317
- _InnerKey.CARBON_SOURCES,
318
- _InnerKey.TILLAGE_CATEGORY,
290
+ class IpccManagementCategory(Enum):
291
+ """
292
+ Enum representing IPCC Management Categories for grasslands and annual croplands.
293
+
294
+ See [IPCC (2019) Vol. 4, Ch. 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more
295
+ information.
296
+ """
297
+ SEVERELY_DEGRADED = "severely degraded"
298
+ IMPROVED_GRASSLAND = "improved grassland"
299
+ HIGH_INTENSITY_GRAZING = "high-intensity grazing"
300
+ NOMINALLY_MANAGED = "nominally managed"
301
+ FULL_TILLAGE = "full tillage"
302
+ REDUCED_TILLAGE = "reduced tillage"
303
+ NO_TILLAGE = "no tillage"
304
+ OTHER = "other"
305
+
306
+
307
+ class _InventoryKey(Enum):
308
+ """
309
+ Enum representing the inner keys of the annual inventory is constructed from site and cycle data.
310
+ """
311
+ # Tier 1
312
+ LU_CATEGORY = 'ipcc land use category'
313
+ MG_CATEGORY = 'ipcc management category'
314
+ CI_CATEGORY = 'ipcc carbon input category'
315
+ SHOULD_RUN_TIER_1 = 'should run tier 1'
316
+ # Tier 2
317
+ TEMP_MONTHLY = 'temperature monthly'
318
+ PRECIP_MONTHLY = 'precipitation monthly'
319
+ PET_MONTHLY = 'PET monthly'
320
+ IRRIGATED_MONTHLY = 'irrigated monthly'
321
+ CARBON_INPUT = 'carbon input'
322
+ N_CONTENT = 'nitrogen content'
323
+ LIGNIN_CONTENT = 'lignin content'
324
+ TILLAGE_CATEGORY = 'ipcc tillage category'
325
+ SAND_CONTENT = 'sand content'
326
+ IS_PADDY_RICE = 'is paddy rice'
327
+ SHOULD_RUN_TIER_2 = 'should run tier 2'
328
+
329
+
330
+ REQUIRED_KEYS_TIER_1 = [
331
+ _InventoryKey.LU_CATEGORY,
332
+ _InventoryKey.MG_CATEGORY,
333
+ _InventoryKey.CI_CATEGORY
319
334
  ]
320
335
 
321
- INNER_KEYS_RUN_WITHOUT_IRRIGATION = [
322
- _InnerKey.TEMPERATURES,
323
- _InnerKey.PRECIPITATIONS,
324
- _InnerKey.PETS,
325
- _InnerKey.CARBON_SOURCES,
326
- _InnerKey.TILLAGE_CATEGORY,
336
+
337
+ REQUIRED_KEYS_TIER_2 = [
338
+ _InventoryKey.TEMP_MONTHLY,
339
+ _InventoryKey.PRECIP_MONTHLY,
340
+ _InventoryKey.PET_MONTHLY,
341
+ _InventoryKey.CARBON_INPUT,
342
+ _InventoryKey.N_CONTENT,
343
+ _InventoryKey.LIGNIN_CONTENT,
344
+ _InventoryKey.TILLAGE_CATEGORY,
345
+ _InventoryKey.IS_PADDY_RICE
327
346
  ]
328
347
 
329
348
 
330
349
  # --- TIER 1 ENUMS ---
331
350
 
332
351
 
333
- IpccSoilCategory = Enum("IpccSoilCategory", [
334
- "ORGANIC_SOILS",
335
- "SANDY_SOILS",
336
- "WETLAND_SOILS",
337
- "VOLCANIC_SOILS",
338
- "SPODIC_SOILS",
339
- "HIGH_ACTIVITY_CLAY_SOILS",
340
- "LOW_ACTIVITY_CLAY_SOILS",
341
- ])
342
- """
343
- Enum representing IPCC Soil Categories.
352
+ class IpccSoilCategory(Enum):
353
+ """
354
+ Enum representing IPCC Soil Categories.
344
355
 
345
- See [IPCC (2019) Vol 4, Ch. 2 and 3](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
346
- """
356
+ See [IPCC (2019) Vol 4, Ch. 2 and 3](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more
357
+ information.
358
+ """
359
+ ORGANIC_SOILS = "organic soils"
360
+ SANDY_SOILS = "sandy soils"
361
+ WETLAND_SOILS = "wetland soils"
362
+ VOLCANIC_SOILS = "volcanic soils"
363
+ SPODIC_SOILS = "spodic soils"
364
+ HIGH_ACTIVITY_CLAY_SOILS = "high-activity clay soils"
365
+ LOW_ACTIVITY_CLAY_SOILS = "low-activity clay soils"
347
366
 
348
367
 
349
- IpccLandUseCategory = Enum("IpccLandUseCategory", [
350
- "GRASSLAND",
351
- "PERENNIAL_CROPS",
352
- "PADDY_RICE_CULTIVATION",
353
- "ANNUAL_CROPS_WET",
354
- "ANNUAL_CROPS",
355
- "SET_ASIDE",
356
- "FOREST",
357
- "NATIVE",
358
- "OTHER"
359
- ])
360
- """
361
- Enum representing IPCC Land Use Categories.
368
+ class IpccLandUseCategory(Enum):
369
+ """
370
+ Enum representing IPCC Land Use Categories.
362
371
 
363
- See [IPCC (2019) Vol 4](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
364
- """
372
+ See [IPCC (2019) Vol 4](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
373
+ """
374
+ GRASSLAND = "grassland"
375
+ PERENNIAL_CROPS = "perennial crops"
376
+ PADDY_RICE_CULTIVATION = "paddy rice cultivation"
377
+ ANNUAL_CROPS_WET = "annual crops (wet)"
378
+ ANNUAL_CROPS = "annual crops"
379
+ SET_ASIDE = "set aside"
380
+ FOREST = "forest"
381
+ NATIVE = "native"
382
+ OTHER = "other"
365
383
 
366
384
 
367
- IpccCarbonInputCategory = Enum("IpccCarbonInputCategory", [
368
- "GRASSLAND_HIGH",
369
- "GRASSLAND_MEDIUM",
370
- "CROPLAND_HIGH_WITH_MANURE",
371
- "CROPLAND_HIGH_WITHOUT_MANURE",
372
- "CROPLAND_MEDIUM",
373
- "CROPLAND_LOW",
374
- "OTHER"
375
- ])
376
- """
377
- Enum representing IPCC Carbon Input Categories for improved grasslands
378
- and annual croplands.
385
+ class IpccCarbonInputCategory(Enum):
386
+ """
387
+ Enum representing IPCC Carbon Input Categories for improved grasslands and annual croplands.
379
388
 
380
- See [IPCC (2019) Vol. 4, Ch. 4, 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html)
381
- for more information.
382
- """
389
+ See [IPCC (2019) Vol. 4, Ch. 4, 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more
390
+ information.
391
+ """
392
+ GRASSLAND_HIGH = "grassland high"
393
+ GRASSLAND_MEDIUM = "grassland medium"
394
+ CROPLAND_HIGH_WITH_MANURE = "cropland high (with manure)"
395
+ CROPLAND_HIGH_WITHOUT_MANURE = "cropland high (without manure)"
396
+ CROPLAND_MEDIUM = "cropland medium"
397
+ CROPLAND_LOW = "cropland low"
398
+ OTHER = "other"
383
399
 
384
400
 
385
401
  # --- TIER 2 NAMED TUPLES FOR CARBON SOURCES AND MODEL RESULTS ---
@@ -448,33 +464,6 @@ annual_water_factors : list[float]
448
464
  """
449
465
 
450
466
 
451
- CarbonInputResult = NamedTuple(
452
- "CarbonInputResult",
453
- [
454
- ("timestamps", list[float]),
455
- ("organic_carbon_inputs", list[float]),
456
- ("average_nitrogen_contents", list[float]),
457
- ("average_lignin_contents", list[float]),
458
- ]
459
- )
460
- """
461
- A named tuple to hold the result of `_run_annual_organic_carbon_inputs`.
462
-
463
- Attributes
464
- ----------
465
- timestamps : list[int]
466
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
467
- organic_carbon_inputs : list[float]
468
- A list of organic carbon inputs to the soil for each year in the inventory, kg C ha-1.
469
- average_nitrogen_contents : list[float]
470
- A list of the average nitrogen contents of the carbon sources for each year in the inventory, decimal_proportion,
471
- kg N (kg d.m.)-1.
472
- average_lignin_contents : list[float]
473
- A list of the average lignin contents of the carbon sources for each year in the inventory, decimal_proportion,
474
- kg lignin (kg d.m.)-1.
475
- """
476
-
477
-
478
467
  Tier2SocResult = NamedTuple(
479
468
  "Tier2SocResult",
480
469
  [
@@ -522,45 +511,6 @@ carbon_input_factor : float
522
511
  The stock change factor for mineral soil organic C for the input of organic amendments, dimensionless.
523
512
  """
524
513
 
525
- CarbonInputArgs = NamedTuple("CarbonInputArgs", [
526
- ("num_grassland_improvements", int),
527
- ("has_irrigation", bool),
528
- ("has_residue_removed_or_burnt", bool),
529
- ("has_low_residue_producing_crops", bool),
530
- ("has_bare_fallow", bool),
531
- ("has_n_fixing_crop_or_inorganic_n_fertiliser_used", bool),
532
- ("has_practice_increasing_c_input", bool),
533
- ("has_cover_crop", bool),
534
- ("has_organic_fertiliser_or_soil_amendment_used", bool),
535
- ("has_animal_manure_used", bool)
536
- ])
537
- """
538
- Named tuple representing the arguments for the functions assigning `IpccCarbonInputCategories` to inventory years.
539
-
540
- Attributes
541
- ----------
542
- num_grassland_improvements : int
543
- The number of grassland improvements.
544
- has_irrigation : bool
545
- Indicates whether irrigation is applied to more than 30% of the site.
546
- has_residue_removed_or_burnt : bool
547
- Indicates whether residues are removed or burnt on more than 30% of the site.
548
- has_low_residue_producing_crops : bool
549
- Indicates whether low residue-producing crops are present on more than 70% of the site.
550
- has_bare_fallow : bool
551
- Indicates whether bare fallow is present on more than 30% of the site.
552
- has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
553
- Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
554
- has_practice_increasing_c_input : bool
555
- Indicates whether practices increasing carbon input are present on more than 30% of the site.
556
- has_cover_crop : bool
557
- Indicates whether cover crops are present on more than 30% of the site.
558
- has_organic_fertiliser_or_soil_amendment_used : bool
559
- Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
560
- has_animal_manure_used : bool
561
- Indicates whether animal manure is used on more than 30% of the site.
562
- """
563
-
564
514
 
565
515
  # --- SHARED TIER 1 & TIER 2 MAPPING DICTS ---
566
516
 
@@ -650,8 +600,8 @@ IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE = {
650
600
  IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: "Low-activity clay soils",
651
601
  }
652
602
  """
653
- A dictionary mapping IPCC soil categories to corresponding soil type and USDA soil type lookup values
654
- in the `"IPCC_SOIL_CATEGORY"` column.
603
+ A dictionary mapping IPCC soil categories to corresponding soil type and USDA soil type lookup values in the
604
+ `"IPCC_SOIL_CATEGORY"` column.
655
605
  """
656
606
 
657
607
  IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE = {
@@ -675,8 +625,8 @@ IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE = {
675
625
  IpccLandUseCategory.ANNUAL_CROPS: "Annual crops"
676
626
  }
677
627
  """
678
- A dictionary mapping IPCC land use categories to corresponding land cover lookup values
679
- in the `"IPCC_LAND_USE_CATEGORY"` column.
628
+ A dictionary mapping IPCC land use categories to corresponding land cover lookup values in the
629
+ `"IPCC_LAND_USE_CATEGORY"` column.
680
630
  """
681
631
 
682
632
  IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID = {
@@ -687,8 +637,8 @@ IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID = {
687
637
  IpccManagementCategory.OTHER: "nativePasture"
688
638
  }
689
639
  """
690
- A dictionary mapping IPCC management categories to corresponding grassland management term IDs from the
691
- land cover glossary.
640
+ A dictionary mapping IPCC management categories to corresponding grassland management term IDs from the land cover
641
+ glossary.
692
642
  """
693
643
 
694
644
 
@@ -745,8 +695,7 @@ def _check_cycle_tillage_management_category(
745
695
  tillage_nodes,
746
696
  lookup=LOOKUP,
747
697
  target_lookup_values=target_lookup_values,
748
- cumulative_threshold=MIN_AREA_THRESHOLD,
749
- default_node_value=DEFAULT_NODE_VALUE
698
+ cumulative_threshold=MIN_AREA_THRESHOLD
750
699
  ) and (
751
700
  key is not IpccManagementCategory.NO_TILLAGE
752
701
  or _check_zero_tillages(tillage_nodes)
@@ -1515,18 +1464,16 @@ def _run_soc_stocks(
1515
1464
  annual_temperature_factors: list[float],
1516
1465
  annual_water_factors: list[float],
1517
1466
  annual_organic_carbon_inputs: list[float],
1518
- annual_average_nitrogen_contents_of_organic_carbon_sources: list[float],
1519
- annual_average_lignin_contents_of_organic_carbon_sources: list[float],
1467
+ annual_n_contents: list[float],
1468
+ annual_lignin_contents: list[float],
1520
1469
  annual_tillage_categories: Union[list[IpccManagementCategory], None] = None,
1521
1470
  sand_content: float = 0.33,
1522
1471
  run_in_period: int = 5,
1523
- initial_soc_stock: Union[float, None] = None,
1524
1472
  params: Union[dict[str, float], None] = None,
1525
1473
  ) -> Tier2SocResult:
1526
1474
  """
1527
1475
  Run the IPCC Tier 2 SOC model with precomputed `annual_temperature_factors`, `annual_water_factors`,
1528
- `annual_organic_carbon_inputs`, `annual_average_nitrogen_contents_of_organic_carbon_sources`,
1529
- `annual_average_lignin_contents_of_organic_carbon_sources`.
1476
+ `annual_organic_carbon_inputs`, `annual_n_contents`, `annual_lignin_contents`.
1530
1477
 
1531
1478
  Parameters
1532
1479
  ----------
@@ -1537,22 +1484,20 @@ def _run_soc_stocks(
1537
1484
  annual_water_factors : list[float]
1538
1485
  A list of water factors for each year in the inventory, dimensionless (see Equation 5.0F).
1539
1486
  annual_organic_carbon_inputs : list[float]
1540
- A list of organic carbon inputs to the soil for each year in the inventory, kg C ha-1 year-1
1541
- (see Equation 5.0H).
1542
- annual_average_nitrogen_contents_of_organic_carbon_sources : list[float]
1543
- A list of the average nitrogen contents of the organic carbon sources for each year in the inventory,
1544
- decimal proportion.
1545
- annual_average_lignin_contents_of_organic_carbon_sources : list[float]
1546
- A list of the average lignin contents of the organic carbon sources for each year in the inventory,
1547
- decimal proportion.
1487
+ A list of organic carbon inputs to the soil for each year in the inventory, kg C ha-1 year-1 (see Equation
1488
+ 5.0H).
1489
+ annual_n_contents : list[float]
1490
+ A list of the average nitrogen contents of the organic carbon sources for each year in the inventory, decimal
1491
+ proportion.
1492
+ annual_lignin_contents : list[float]
1493
+ A list of the average lignin contents of the organic carbon sources for each year in the inventory, decimal
1494
+ proportion.
1548
1495
  annual_tillage_categories : list[IpccManagementCategory] | None
1549
1496
  A list of the site"s `IpccManagementCategory`s for each year in the inventory.
1550
1497
  sand_content : float
1551
1498
  The sand content of the site, decimal proportion, default value: `0.33`.
1552
1499
  run_in_period : int
1553
1500
  The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
1554
- initial_soc_stock : float | None
1555
- The measured or pre-computed initial SOC stock at the end of the run-in period, kg C ha-1.
1556
1501
  params : dict[str: float] | None
1557
1502
  Overrides for the model parameters. If `None` only default parameters will be used.
1558
1503
 
@@ -1597,29 +1542,16 @@ def _run_soc_stocks(
1597
1542
 
1598
1543
  # --- SPLIT ANNUAL DATA INTO RUN-IN AND INVENTORY PERIODS ---
1599
1544
 
1600
- inventory_temperature_factors = timeseries_to_inventory(
1601
- annual_temperature_factors, run_in_period
1602
- )
1603
- inventory_water_factors = timeseries_to_inventory(
1604
- annual_water_factors, run_in_period
1605
- )
1606
- inventory_carbon_inputs = timeseries_to_inventory(
1607
- annual_organic_carbon_inputs, run_in_period
1608
- )
1609
- inventory_nitrogen_contents = timeseries_to_inventory(
1610
- annual_average_nitrogen_contents_of_organic_carbon_sources, run_in_period
1611
- )
1612
- inventory_lignin_contents = timeseries_to_inventory(
1613
- annual_average_lignin_contents_of_organic_carbon_sources, run_in_period
1614
- )
1545
+ inventory_temperature_factors = timeseries_to_inventory(annual_temperature_factors, run_in_period)
1546
+ inventory_water_factors = timeseries_to_inventory(annual_water_factors, run_in_period)
1547
+ inventory_carbon_inputs = timeseries_to_inventory(annual_organic_carbon_inputs, run_in_period)
1548
+ inventory_n_contents = timeseries_to_inventory(annual_n_contents, run_in_period)
1549
+ inventory_lignin_contents = timeseries_to_inventory(annual_lignin_contents, run_in_period)
1615
1550
  inventory_f_2s = timeseries_to_inventory(annual_f_2s, run_in_period)
1616
- inventory_tillage_factors = timeseries_to_inventory(
1617
- annual_tillage_factors, run_in_period
1618
- )
1551
+ inventory_tillage_factors = timeseries_to_inventory(annual_tillage_factors, run_in_period)
1619
1552
 
1620
- inventory_timestamps = timestamps[
1621
- run_in_period - 1:
1622
- ] # The last year of the run-in should be the first year of the inventory
1553
+ # The last year of the run-in should be the first year of the inventory
1554
+ inventory_timestamps = timestamps[run_in_period - 1:]
1623
1555
 
1624
1556
  # --- CALCULATE THE ACTIVE ACTIVE POOL STEADY STATES ---
1625
1557
 
@@ -1641,7 +1573,7 @@ def _run_soc_stocks(
1641
1573
  inventory_carbon_inputs,
1642
1574
  inventory_f_2s,
1643
1575
  inventory_lignin_contents,
1644
- inventory_nitrogen_contents,
1576
+ inventory_n_contents,
1645
1577
  )
1646
1578
  ]
1647
1579
 
@@ -1731,39 +1663,9 @@ def _run_soc_stocks(
1731
1663
 
1732
1664
  # --- CALCULATE THE ACTIVE, SLOW AND PASSIVE SOC STOCKS ---
1733
1665
 
1734
- init_total_steady_state = (
1735
- inventory_active_pool_steady_states[0] +
1736
- inventory_slow_pool_steady_states[0] + inventory_passive_pool_steady_states[0]
1737
- )
1738
-
1739
- init_active_frac = inventory_active_pool_steady_states[0]/init_total_steady_state
1740
- init_slow_frac = inventory_slow_pool_steady_states[0]/init_total_steady_state
1741
- init_passive_frac = 1 - (init_active_frac + init_slow_frac)
1742
-
1743
- inventory_active_pool_soc_stocks = []
1744
- inventory_slow_pool_soc_stocks = []
1745
- inventory_passive_pool_soc_stocks = []
1746
-
1747
- should_calc_run_in = initial_soc_stock is None
1748
-
1749
- inventory_active_pool_soc_stocks.insert(
1750
- 0,
1751
- inventory_active_pool_steady_states[0]
1752
- if should_calc_run_in
1753
- else init_active_frac * initial_soc_stock,
1754
- )
1755
- inventory_slow_pool_soc_stocks.insert(
1756
- 0,
1757
- inventory_slow_pool_steady_states[0]
1758
- if should_calc_run_in
1759
- else init_slow_frac * initial_soc_stock,
1760
- )
1761
- inventory_passive_pool_soc_stocks.insert(
1762
- 0,
1763
- inventory_passive_pool_steady_states[0]
1764
- if should_calc_run_in
1765
- else init_passive_frac * initial_soc_stock,
1766
- )
1666
+ inventory_active_pool_soc_stocks = inventory_active_pool_steady_states[:1]
1667
+ inventory_slow_pool_soc_stocks = inventory_slow_pool_steady_states[:1]
1668
+ inventory_passive_pool_soc_stocks = inventory_passive_pool_steady_states[:1]
1767
1669
 
1768
1670
  for index in range(1, len(inventory_timestamps), 1):
1769
1671
  inventory_active_pool_soc_stocks.insert(
@@ -1804,26 +1706,6 @@ def _run_soc_stocks(
1804
1706
  # --- TIER 2 SUB-MODEL: ANNUAL TEMPERATURE FACTORS ---
1805
1707
 
1806
1708
 
1807
- def _check_consecutive(ints: list[int]) -> bool:
1808
- """
1809
- Checks whether a list of integers are consecutive.
1810
-
1811
- Used to determine whether annualised data is complete from every year from beggining to end.
1812
-
1813
- Parameters
1814
- ----------
1815
- ints : list[int]
1816
- A list of integer values.
1817
-
1818
- Returns
1819
- -------
1820
- bool
1821
- Whether or not the list of integers is consecutive.
1822
- """
1823
- range_list = list(range(min(ints), max(ints)+1)) if ints else []
1824
- return all(a == b for a, b in zip(ints, range_list))
1825
-
1826
-
1827
1709
  def _check_12_months(inner_dict: dict, keys: set[Any]):
1828
1710
  """
1829
1711
  Checks whether an inner dict has 12 months of data for each of the required inner keys.
@@ -1848,45 +1730,6 @@ def _check_12_months(inner_dict: dict, keys: set[Any]):
1848
1730
  # --- SUB-MODEL ANNUAL TEMPERATURE FACTORS ---
1849
1731
 
1850
1732
 
1851
- def _should_run_annual_temperature_factors(
1852
- site: dict
1853
- ) -> tuple[bool, dict]:
1854
- """
1855
- Extracts, formats and checks data from the site node to determine whether or not to run the annual temperature
1856
- factors model on a specific Hestia `Site`.
1857
-
1858
- Parameters
1859
- ----------
1860
- site : dict
1861
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
1862
-
1863
- Returns
1864
- -------
1865
- tuple[bool, dict]
1866
- `(should_run, grouped_data)`.
1867
- """
1868
- measurements = site.get("measurements", [])
1869
- temperature_monthly = find_term_match(measurements, TEMPERATURE_MONTHLY_TERM_ID, {})
1870
-
1871
- grouped_data = group_measurement_values_by_year(
1872
- temperature_monthly,
1873
- inner_key=_InnerKey.TEMPERATURES,
1874
- complete_years_only=True
1875
- )
1876
-
1877
- should_run = all([
1878
- all(
1879
- _check_12_months(inner, {_InnerKey.TEMPERATURES})
1880
- for inner in grouped_data.values()
1881
- ),
1882
- len(grouped_data.keys()) >= MIN_RUN_IN_PERIOD,
1883
- _check_consecutive(grouped_data.keys())
1884
- ])
1885
-
1886
- logShouldRun(site, MODEL, TERM_ID, should_run, sub_model="_run_annual_temperature_factors")
1887
- return should_run, grouped_data
1888
-
1889
-
1890
1733
  def _run_annual_temperature_factors(
1891
1734
  timestamps: list[int],
1892
1735
  temperatures: list[list[float]],
@@ -1926,77 +1769,6 @@ def _run_annual_temperature_factors(
1926
1769
  # --- TIER 2 SUB-MODEL: ANNUAL WATER FACTORS ---
1927
1770
 
1928
1771
 
1929
- def _should_run_annual_water_factors(
1930
- site: dict,
1931
- cycles: list[dict]
1932
- ) -> tuple[bool, bool, dict]:
1933
- """
1934
- Extracts, formats and checks data from the site and cycle nodes to determine determine whether or not to run the
1935
- annual water factors model on a specific Hestia `Site` and `Cycle`s.
1936
-
1937
- TODO: Implement checks for monthly is_irrigateds from cycles.
1938
-
1939
- Parameters
1940
- ----------
1941
- site : dict
1942
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
1943
- cycles : list[dict]
1944
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
1945
-
1946
- Returns
1947
- -------
1948
- tuple[bool, bool, dict]
1949
- `(should_run, run_with_irrigation, grouped_data)`.
1950
- """
1951
- measurements = site.get("measurements", [])
1952
- precipitation_monthly = find_term_match(measurements, PRECIPITATION_MONTHLY_TERM_ID, {})
1953
- potential_evapotranspiration_monthly = find_term_match(measurements, PET_MONTHLY_TERM_ID, {})
1954
-
1955
- grouped_precipitations = group_measurement_values_by_year(
1956
- precipitation_monthly,
1957
- inner_key=_InnerKey.PRECIPITATIONS,
1958
- complete_years_only=True
1959
- )
1960
- grouped_pets = group_measurement_values_by_year(
1961
- potential_evapotranspiration_monthly,
1962
- inner_key=_InnerKey.PETS,
1963
- complete_years_only=True
1964
- )
1965
-
1966
- is_irrigateds = None # TODO: Implement is_irrigateds check.
1967
- run_with_irrigation = bool(is_irrigateds)
1968
-
1969
- grouped_data = (
1970
- merge(grouped_precipitations, grouped_pets) if is_irrigateds is None else reduce(
1971
- merge, [grouped_precipitations, grouped_pets, is_irrigateds]
1972
- )
1973
- )
1974
-
1975
- should_run = all([
1976
- all(
1977
- _check_12_months(inner, {_InnerKey.PRECIPITATIONS, _InnerKey.PETS})
1978
- for inner in grouped_data.values()
1979
- ),
1980
- not run_with_irrigation or all(
1981
- _check_12_months(inner, {_InnerKey.IS_IRRIGATEDS})
1982
- for inner in grouped_data.values()
1983
- ),
1984
- len(grouped_data.keys()) >= MIN_RUN_IN_PERIOD,
1985
- _check_consecutive(grouped_data.keys()),
1986
- check_cycle_site_ids_identical(cycles)
1987
- ])
1988
-
1989
- logShouldRun(
1990
- site,
1991
- MODEL,
1992
- TERM_ID,
1993
- should_run,
1994
- sub_model="_run_annual_water_factors",
1995
- run_with_irrigation=run_with_irrigation
1996
- )
1997
- return should_run, run_with_irrigation, grouped_data
1998
-
1999
-
2000
1772
  def _run_annual_water_factors(
2001
1773
  timestamps: list[int],
2002
1774
  precipitations: list[list[float]],
@@ -2109,338 +1881,130 @@ def _get_carbon_sources_from_cycles(cycles: dict) -> list[CarbonSource]:
2109
1881
  return non_empty_list([_iterate_carbon_source(node) for node in inputs_and_products])
2110
1882
 
2111
1883
 
2112
- def _should_run_annual_organic_carbon_inputs(
2113
- site: dict,
2114
- cycles: list[dict]
2115
- ) -> tuple[bool, dict]:
2116
- """
2117
- Extracts, formats and checks data from the site node to determine whether or not to run the annual organic carbon
2118
- inputs model on a specific set of Hestia `Cycle`s.
1884
+ # --- TIER 2 SOC MODEL ---
2119
1885
 
2120
- Parameters
2121
- ----------
2122
- cycles : list[dict]
2123
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
2124
1886
 
2125
- Returns
2126
- -------
2127
- tuple[bool, dict]
2128
- `(should_run, grouped_data)`.
1887
+ def _run_tier_2(
1888
+ inventory: dict[int: dict[_InventoryKey: any]],
1889
+ *,
1890
+ run_in_period: int = 5,
1891
+ run_with_irrigation: bool = True,
1892
+ sand_content: float = 0.33,
1893
+ params: Union[dict[str, float], None] = None,
1894
+ **_
1895
+ ) -> list[dict]:
2129
1896
  """
2130
- grouped_cycles = group_nodes_by_year(cycles)
2131
-
2132
- grouped_data = {
2133
- year: {
2134
- _InnerKey.CARBON_SOURCES: _get_carbon_sources_from_cycles(_cycles)
2135
- } for year, _cycles in grouped_cycles.items()
1897
+ Run the IPCC Tier 2 SOC model on a time series of annual data about a site and the mangagement activities taking
1898
+ place on it. To avoid any errors, the `inventory` parameter must be pre-validated by the `should_run` function.
1899
+
1900
+ The inventory should be in the following shape:
1901
+ ```
1902
+ {
1903
+ year (int): {
1904
+ _InventoryKey.SHOULD_RUN_TIER_2: bool,
1905
+ _InventoryKey.TEMP_MONTHLY: list[float],
1906
+ _InventoryKey.PRECIP_MONTHLY: list[float],
1907
+ _InventoryKey.PET_MONTHLY: list[float],
1908
+ _InventoryKey.IRRIGATED_MONTHLY: list[bool]
1909
+ _InventoryKey.CARBON_INPUT: float,
1910
+ _InventoryKey.N_CONTENT: float,
1911
+ _InventoryKey.TILLAGE_CATEGORY: IpccManagementCategory,
1912
+ _InventoryKey.SAND_CONTENT: float
1913
+ },
1914
+ ...
2136
1915
  }
1916
+ ```
2137
1917
 
2138
- should_run = all([
2139
- len(grouped_data.keys()) >= MIN_RUN_IN_PERIOD,
2140
- _check_consecutive(grouped_data.keys()),
2141
- check_cycle_site_ids_identical(cycles)
2142
- ])
2143
-
2144
- logShouldRun(site, MODEL, TERM_ID, should_run, sub_model="_run_annual_organic_carbon_inputs")
2145
- return should_run, grouped_data
2146
-
2147
-
2148
- def _run_annual_organic_carbon_inputs(
2149
- timestamps: list[int],
2150
- annual_carbon_sources: list[list[CarbonSource]],
2151
- default_carbon_content: float = 0.42,
2152
- default_nitrogen_content: float = 0.0085,
2153
- default_lignin_content: float = 0.073,
2154
- ):
2155
- """
2156
- Calculate the organic carbon input, average nitrogen content of carbon sources and average lignin content of carbon
2157
- sources for each year of the inventory.
2158
-
2159
- `timestamps` and `annual_carbon_sources` must have the same length.
1918
+ TODO: interpolate between `sandContent` measurements for different years of the inventory
2160
1919
 
2161
1920
  Parameters
2162
1921
  ----------
2163
- timestamps : list[int]
2164
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
2165
- annual_carbon_sources : list[list[CarbonSource]]
2166
- A list of carbon sources for each year of the inventory, where each carbon source is a named tupled with the
2167
- format `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`
2168
- default_carbon_content : float
2169
- The default carbon content of a carbon source, decimal proportion, kg C (kg d.m.)-1, default value: `0.42`.
2170
- default_nitrogen_content : float
2171
- The default nitrogen content of a carbon source, decimal proportion, kg N (kg d.m.)-1, default value: `0.0085`.
2172
- default_lignin_content : float)
2173
- The default lignin content of a carbon source, decimal proportion, kg lignin (kg d.m.)-1,
2174
- default value: `0.073`.
1922
+ inventory : dict
1923
+ The inventory built by the `_should_run` function.
1924
+ run_in_period : int, optional
1925
+ The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
1926
+ run_with_irrigation : bool, optional
1927
+ `True` if the model should run while taking into account irrigation, `False` if not.
1928
+ params : dict | None, optional
1929
+ Overrides for the model parameters. If `None` only default parameters will be used.
2175
1930
 
2176
1931
  Returns
2177
1932
  -------
2178
- CarbonInputResult
2179
- An inventory of annual carbon input data as a named tuple with the format
2180
- `(timestamps: list[int], organic_carbon_inputs: list[float], average_nitrogen_contents: list[float],
2181
- average_lignin_contents: list[float])`
2182
- """
2183
- return CarbonInputResult(
2184
- timestamps=timestamps,
2185
- organic_carbon_inputs=[
2186
- _calc_total_organic_carbon_input(sources, default_carbon_content=default_carbon_content)
2187
- for sources in annual_carbon_sources
2188
- ],
2189
- average_nitrogen_contents=[
2190
- _calc_average_nitrogen_content_of_organic_carbon_sources(
2191
- sources, default_nitrogen_content=default_nitrogen_content)
2192
- for sources in annual_carbon_sources
2193
- ],
2194
- average_lignin_contents=[
2195
- _calc_average_lignin_content_of_organic_carbon_sources(
2196
- sources, default_lignin_content=default_lignin_content)
2197
- for sources in annual_carbon_sources
2198
- ],
2199
- )
2200
-
2201
-
2202
- # --- TIER 2 SOC MODEL ---
2203
-
2204
-
2205
- def _should_run_tier_2(
2206
- site: dict
2207
- ) -> tuple:
1933
+ list[dict]
1934
+ A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
2208
1935
  """
2209
- Extracts, formats and checks data from the site and cycle nodes to determine
2210
- determine whether or not to run the Tier 2 SOC model.
1936
+ valid_inventory = {
1937
+ year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_2)
1938
+ }
2211
1939
 
2212
- Parameters
2213
- ----------
2214
- site : dict
2215
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
2216
- cycles : list[dict]
2217
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
1940
+ timestamps = [year for year in valid_inventory.keys()]
2218
1941
 
2219
- Returns
2220
- -------
2221
- tuple
2222
- `(should_run, timestamps, temperatures, precipitations, pets, carbon_sources, tillage_categories, sand_content,
2223
- is_irrigateds, run_in_period, initial_soc_stock)`
2224
- """
2225
- cycles = related_cycles(site.get("@id"))
1942
+ annual_temperature_monthlys = [group[_InventoryKey.TEMP_MONTHLY] for group in valid_inventory.values()]
1943
+ annual_precipitation_monthlys = [group[_InventoryKey.PRECIP_MONTHLY] for group in valid_inventory.values()]
1944
+ annual_pet_monthlys = [group[_InventoryKey.PET_MONTHLY] for group in valid_inventory.values()]
2226
1945
 
2227
- should_run_t, grouped_temperature_data = _should_run_annual_temperature_factors(site)
2228
- should_run_w, run_with_irrigation, grouped_water_data = _should_run_annual_water_factors(site, cycles)
2229
- should_run_c, grouped_carbon_sources_data = _should_run_annual_organic_carbon_inputs(site, cycles)
1946
+ annual_carbon_inputs = [group[_InventoryKey.CARBON_INPUT] for group in valid_inventory.values()]
1947
+ annual_n_contents = [group[_InventoryKey.N_CONTENT] for group in valid_inventory.values()]
1948
+ annual_lignin_contents = [group[_InventoryKey.LIGNIN_CONTENT] for group in valid_inventory.values()]
1949
+ annual_tillage_categories = [group[_InventoryKey.TILLAGE_CATEGORY] for group in valid_inventory.values()]
1950
+ annual_irrigated_monthly = (
1951
+ [group[_InventoryKey.IRRIGATED_MONTHLY] for group in valid_inventory.values()] if run_with_irrigation else None
1952
+ )
2230
1953
 
2231
- grouped_cycles = group_nodes_by_year(cycles)
1954
+ sand_content = next(
1955
+ group[_InventoryKey.SAND_CONTENT]/100 for group in valid_inventory.values()
1956
+ if _InventoryKey.SAND_CONTENT in group
1957
+ )
2232
1958
 
2233
- grouped_tillage_categories = {
2234
- year: {
2235
- _InnerKey.TILLAGE_CATEGORY: _assign_tier_2_ipcc_tillage_management_category(_cycles)
2236
- } for year, _cycles in grouped_cycles.items()
2237
- }
1959
+ # --- MERGE ANY USER-SET PARAMETERS WITH THE IPCC DEFAULTS ---
2238
1960
 
2239
- # Combine all the grouped data into one dictionary
2240
- grouped_data = reduce(merge, [grouped_temperature_data, grouped_water_data,
2241
- grouped_carbon_sources_data, grouped_tillage_categories])
1961
+ params = DEFAULT_PARAMS | (params or {})
2242
1962
 
2243
- # Select the correct keys for data completeness based on `run_with_irrigation`
2244
- keys = INNER_KEYS_RUN_WITH_IRRIGATION if run_with_irrigation else INNER_KEYS_RUN_WITHOUT_IRRIGATION
1963
+ # --- COMPUTE FACTORS AND CARBON INPUTS ---
2245
1964
 
2246
- # Filter out any incomplete years
2247
- complete_data = dict(filter(
2248
- lambda item: all([key in item[1].keys() for key in keys]),
2249
- grouped_data.items()
2250
- ))
1965
+ _, annual_temperature_factors = _run_annual_temperature_factors(
1966
+ timestamps,
1967
+ annual_temperature_monthlys,
1968
+ maximum_temperature=params.get("maximum_temperature"),
1969
+ optimum_temperature=params.get("optimum_temperature")
1970
+ )
2251
1971
 
2252
- timestamps = list(complete_data)
2253
- start_year = timestamps[0] if timestamps else 0
2254
- end_year = timestamps[-1] if timestamps else 0
2255
-
2256
- measurements = site.get("measurements", [])
2257
-
2258
- sand_content_value, _ = most_relevant_measurement_value_by_depth_and_date(
2259
- measurements,
2260
- SAND_CONTENT_TERM_ID,
2261
- f"{start_year}-12-31",
2262
- DEPTH_UPPER,
2263
- DEPTH_LOWER,
2264
- depth_strict=False
2265
- ) if timestamps else (None, None)
2266
- sand_content = sand_content_value/100 if sand_content_value else None
2267
-
2268
- initial_soc_stock_value, initial_soc_stock_date = most_relevant_measurement_value_by_depth_and_date(
2269
- measurements,
2270
- TERM_ID,
2271
- f"{end_year}-12-31",
2272
- DEPTH_UPPER,
2273
- DEPTH_LOWER,
2274
- depth_strict=True
2275
- ) if timestamps else (None, None)
2276
-
2277
- run_with_initial_soc_stock = bool(initial_soc_stock_value and initial_soc_stock_date)
2278
-
2279
- run_in_period = (
2280
- int(abs(diff_in_years(f"{start_year}-12-31", initial_soc_stock_date)) + 1)
2281
- if run_with_initial_soc_stock else MIN_RUN_IN_PERIOD
2282
- ) if timestamps else 0
2283
-
2284
- timestamps = list(complete_data.keys())
2285
- temperatures = [complete_data[year][_InnerKey.TEMPERATURES] for year in timestamps]
2286
- precipitations = [complete_data[year][_InnerKey.PRECIPITATIONS] for year in timestamps]
2287
- pets = [complete_data[year][_InnerKey.PETS] for year in timestamps]
2288
- annual_carbon_sources = [complete_data[year][_InnerKey.CARBON_SOURCES] for year in timestamps]
2289
- annual_tillage_categories = [complete_data[year][_InnerKey.TILLAGE_CATEGORY] for year in timestamps]
2290
- is_irrigateds = (
2291
- [complete_data[year][_InnerKey.IS_IRRIGATEDS] for year in timestamps] if run_with_irrigation else None
1972
+ _, annual_water_factors = _run_annual_water_factors(
1973
+ timestamps,
1974
+ annual_precipitation_monthlys,
1975
+ annual_pet_monthlys,
1976
+ annual_irrigated_monthly,
1977
+ water_factor_slope=params.get("water_factor_slope")
2292
1978
  )
2293
1979
 
2294
- should_run = all([
2295
- should_run_t,
2296
- should_run_w,
2297
- should_run_c,
2298
- sand_content is not None and 0 < sand_content <= 1,
2299
- run_in_period >= MIN_RUN_IN_PERIOD,
2300
- len(timestamps) >= run_in_period,
2301
- check_cycle_site_ids_identical(cycles),
2302
- _check_consecutive(timestamps)
2303
- ])
1980
+ # --- RUN THE MODEL ---
2304
1981
 
2305
- logShouldRun(
2306
- site,
2307
- MODEL,
2308
- TERM_ID,
2309
- should_run,
2310
- sub_model="_run_tier_2",
2311
- run_with_irrigation=run_with_irrigation,
2312
- run_with_initial_soc_stock=run_with_initial_soc_stock,
2313
- run_in_period=run_in_period
1982
+ result = _run_soc_stocks(
1983
+ timestamps=timestamps,
1984
+ annual_temperature_factors=annual_temperature_factors,
1985
+ annual_water_factors=annual_water_factors,
1986
+ annual_organic_carbon_inputs=annual_carbon_inputs,
1987
+ annual_n_contents=annual_n_contents,
1988
+ annual_lignin_contents=annual_lignin_contents,
1989
+ annual_tillage_categories=annual_tillage_categories,
1990
+ sand_content=sand_content,
1991
+ run_in_period=run_in_period,
1992
+ params=params
2314
1993
  )
2315
1994
 
2316
- return (
2317
- should_run,
2318
- timestamps,
2319
- temperatures,
2320
- precipitations,
2321
- pets,
2322
- annual_carbon_sources,
2323
- annual_tillage_categories,
2324
- sand_content,
2325
- is_irrigateds,
2326
- run_in_period,
2327
- initial_soc_stock_value
2328
- )
1995
+ values = [
1996
+ _calc_tier_2_soc_stock(
1997
+ active,
1998
+ slow,
1999
+ passive
2000
+ ) for active, slow, passive in zip(
2001
+ result.active_pool_soc_stocks,
2002
+ result.slow_pool_soc_stocks,
2003
+ result.passive_pool_soc_stocks
2004
+ )
2005
+ ]
2329
2006
 
2330
-
2331
- def _run_tier_2(
2332
- timestamps: list[int],
2333
- temperatures: list[list[float]],
2334
- precipitations: list[list[float]],
2335
- pets: list[list[float]],
2336
- annual_carbon_sources: list[list[CarbonSource]],
2337
- annual_tillage_categories: list[IpccManagementCategory],
2338
- sand_content: float = 0.33,
2339
- is_irrigateds: Union[list[list[bool]], None] = None,
2340
- run_in_period: int = 5,
2341
- initial_soc_stock: Union[float, None] = None,
2342
- params: Union[dict[str, float], None] = None,
2343
- ) -> list[dict]:
2344
- """
2345
- Run the IPCC Tier 2 SOC model on a time series of annual data about a site and the mangagement activities taking
2346
- place on it. `timestamps` and `annual_`... lists must be the same length.
2347
-
2348
- Parameters
2349
- ----------
2350
- timestamps : list[int]
2351
- A list of integer timestamps (e.g. [1995, 1996...]) for each year in the inventory.
2352
- temperatures : list[list[float]]
2353
- A list of monthly average temperatures for each year in the inventory.
2354
- precipitations : list[list[float]]
2355
- A list of monthly sum precipitations for each year in the inventory.
2356
- pets : list[list[float]]
2357
- A list of monthly sum potential evapotransiprations for each year in the inventory.
2358
- annual_carbon_sources : list[list[CarbonSource]]
2359
- A list of carbon sources for each year of the inventory, where each carbon source is a named tupled with the
2360
- format `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`
2361
- annual_tillage_categories : list[IpccManagementCategory)
2362
- A list of the site"s IpccManagementCategory for each year in the inventory.
2363
- sand_content : float
2364
- The sand content of the site, decimal proportion, default value: `0.33`.
2365
- is_irrigateds : list[list[bool]] | None
2366
- A list of monthly booleans that describe whether irrigation is used in a particular calendar month for each
2367
- year in the inventory.
2368
- run_in_period : int
2369
- The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
2370
- initial_soc_stock : float | None]
2371
- The measured or pre-computed initial SOC stock at the end of the run-in period, kg C ha-1.
2372
- params : dict | None
2373
- Overrides for the model parameters. If `None` only default parameters will be used.
2374
-
2375
- Returns
2376
- -------
2377
- list[dict]
2378
- A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
2379
- """
2380
-
2381
- # --- MERGE ANY USER-SET PARAMETERS WITH THE IPCC DEFAULTS ---
2382
-
2383
- params = DEFAULT_PARAMS | (params or {})
2384
-
2385
- # --- COMPUTE FACTORS AND CARBON INPUTS ---
2386
-
2387
- _, annual_temperature_factors = _run_annual_temperature_factors(
2388
- timestamps,
2389
- temperatures,
2390
- maximum_temperature=params.get("maximum_temperature"),
2391
- optimum_temperature=params.get("optimum_temperature")
2392
- )
2393
-
2394
- _, annual_water_factors = _run_annual_water_factors(
2395
- timestamps,
2396
- precipitations,
2397
- pets,
2398
- is_irrigateds,
2399
- water_factor_slope=params.get("water_factor_slope")
2400
- )
2401
-
2402
- (
2403
- _,
2404
- annual_organic_carbon_inputs,
2405
- annual_nitrogen_contents,
2406
- annual_lignin_contents
2407
- ) = _run_annual_organic_carbon_inputs(
2408
- timestamps,
2409
- annual_carbon_sources,
2410
- default_carbon_content=params.get("default_carbon_content"),
2411
- default_nitrogen_content=params.get("default_nitrogen_content"),
2412
- default_lignin_content=params.get("default_lignin_content")
2413
- )
2414
-
2415
- # --- RUN THE MODEL ---
2416
-
2417
- result = _run_soc_stocks(
2418
- timestamps=timestamps,
2419
- annual_temperature_factors=annual_temperature_factors,
2420
- annual_water_factors=annual_water_factors,
2421
- annual_organic_carbon_inputs=annual_organic_carbon_inputs,
2422
- annual_average_nitrogen_contents_of_organic_carbon_sources=annual_nitrogen_contents,
2423
- annual_average_lignin_contents_of_organic_carbon_sources=annual_lignin_contents,
2424
- annual_tillage_categories=annual_tillage_categories,
2425
- sand_content=sand_content,
2426
- run_in_period=run_in_period,
2427
- initial_soc_stock=initial_soc_stock,
2428
- params=params
2429
- )
2430
-
2431
- values = [
2432
- _calc_tier_2_soc_stock(
2433
- active,
2434
- slow,
2435
- passive
2436
- ) for active, slow, passive in zip(
2437
- result.active_pool_soc_stocks,
2438
- result.slow_pool_soc_stocks,
2439
- result.passive_pool_soc_stocks
2440
- )
2441
- ]
2442
-
2443
- # --- RETURN MEASUREMENT NODES ---
2007
+ # --- RETURN MEASUREMENT NODES ---
2444
2008
 
2445
2009
  return [
2446
2010
  _measurement(
@@ -2457,32 +2021,6 @@ def _run_tier_2(
2457
2021
  # --- TIER 1 FUNCTIONS ---
2458
2022
 
2459
2023
 
2460
- def _find_closest_value_index(
2461
- lst: Iterable[Union[int, float]], target_value: Union[int, float]
2462
- ) -> Optional[int]:
2463
- """
2464
- Find the index of the closest value in a list.
2465
-
2466
- Parameters
2467
- ----------
2468
- lst : iterable[int | float]
2469
- The list of integers.
2470
- target : int | float
2471
- The target value.
2472
-
2473
- Returns
2474
- -------
2475
- int
2476
- The index of the closest value.
2477
- """
2478
- should_run = all([
2479
- lst,
2480
- all(isinstance(element, (int, float)) for element in lst),
2481
- isinstance(target_value, (int, float))
2482
- ])
2483
- return min(range(len(lst)), key=lambda i: abs(lst[i] - target_value)) if should_run else None
2484
-
2485
-
2486
2024
  def _retrieve_soc_ref(
2487
2025
  eco_climate_zone: int,
2488
2026
  ipcc_soil_category: IpccSoilCategory
@@ -2519,9 +2057,8 @@ def _retrieve_soc_stock_factors(
2519
2057
  ipcc_carbon_input_category: IpccCarbonInputCategory
2520
2058
  ) -> StockChangeFactors:
2521
2059
  """
2522
- Retrieve the stock change factors for soil organic carbon (SOC)
2523
- based on a given combination of land use, management and carbon
2524
- input.
2060
+ Retrieve the stock change factors for soil organic carbon (SOC) based on a given combination of land use,
2061
+ management and carbon input.
2525
2062
 
2526
2063
  Parameters
2527
2064
  ----------
@@ -2581,8 +2118,8 @@ def _calc_soc_equilibrium(
2581
2118
  """
2582
2119
  Calculate the soil organic carbon (SOC) equilibrium based on reference SOC and factors.
2583
2120
 
2584
- In the tier 1 model, SOC equilibriums are considered to be reached after 20 years of
2585
- consistant land use, management and carbon input.
2121
+ In the tier 1 model, SOC equilibriums are considered to be reached after 20 years of consistant land use,
2122
+ management and carbon input.
2586
2123
 
2587
2124
  Parameters
2588
2125
  ----------
@@ -2648,8 +2185,8 @@ def _iterate_soc_equilibriums(
2648
2185
  timestamps: list[int], soc_equilibriums: list[float]
2649
2186
  ) -> tuple[list[int], list[float]]:
2650
2187
  """
2651
- Iterate over SOC equilibriums, inserting timestamps and soc_equilibriums for any
2652
- missing years where SOC would have reached equilibrium.
2188
+ Iterate over SOC equilibriums, inserting timestamps and soc_equilibriums for any missing years where SOC would have
2189
+ reached equilibrium.
2653
2190
 
2654
2191
  Parameters
2655
2192
  ----------
@@ -2689,7 +2226,6 @@ def _iterate_soc_equilibriums(
2689
2226
  )
2690
2227
 
2691
2228
  for index, (timestamp, soc_equilibrium) in enumerate(zip(timestamps, soc_equilibriums)):
2692
-
2693
2229
  equilibrium_reached_timestamp = calc_equilibrium_reached_timestamp(index)
2694
2230
 
2695
2231
  if is_missing_equilibrium_year(timestamp, equilibrium_reached_timestamp):
@@ -2710,8 +2246,8 @@ def _run_soc_equilibriums(
2710
2246
  """
2711
2247
  Run the soil organic carbon (SOC) equilibriums calculation for each year in the inventory.
2712
2248
 
2713
- Missing years where SOC equilibrium would be reached are inserted to allow for annual
2714
- SOC change to be calculated correctly.
2249
+ Missing years where SOC equilibrium would be reached are inserted to allow for annual SOC change to be calculated
2250
+ correctly.
2715
2251
 
2716
2252
  Parameters
2717
2253
  ----------
@@ -2733,8 +2269,8 @@ def _run_soc_equilibriums(
2733
2269
  Returns
2734
2270
  -------
2735
2271
  tuple[list[int], list[float]]
2736
- `timestamps` and `soc_equilibriums` for each year in the inventory, including any
2737
- missing years where SOC equilibrium would have been reached.
2272
+ `timestamps` and `soc_equilibriums` for each year in the inventory, including any missing years where SOC
2273
+ equilibrium would have been reached.
2738
2274
  """
2739
2275
 
2740
2276
  # Calculate SOC equilibriums for each year
@@ -2767,8 +2303,7 @@ def _calc_tier_1_soc_stocks(
2767
2303
  soc_equilibriums: list[float],
2768
2304
  ) -> list[float]:
2769
2305
  """
2770
- Calculate soil organic carbon (SOC) stocks (kg C ha-1) in the 0-30cm depth interval for each year in
2771
- the inventory.
2306
+ Calculate soil organic carbon (SOC) stocks (kg C ha-1) in the 0-30cm depth interval for each year in the inventory.
2772
2307
 
2773
2308
  Parameters
2774
2309
  ----------
@@ -2809,62 +2344,6 @@ def _calc_tier_1_soc_stocks(
2809
2344
  return soc_stocks
2810
2345
 
2811
2346
 
2812
- def _calc_measurement_scaling_factor(
2813
- measured_soc: float, calculated_soc: float
2814
- ) -> float:
2815
- """
2816
- Calculate the scaling factor soil organic carbon (SOC) values based
2817
- on the ratio between measured and calculated values.
2818
-
2819
- Parameters
2820
- ----------
2821
- measured_soc : float
2822
- The measured SOC value.
2823
- calculated_soc : float
2824
- The calculated SOC value.
2825
-
2826
- Returns
2827
- -------
2828
- float
2829
- The scaling factor.
2830
- """
2831
- return measured_soc / calculated_soc
2832
-
2833
-
2834
- def _scale_soc_stocks(
2835
- soc_stocks: list[float],
2836
- soc_measurement_value: Union[float, None] = None,
2837
- soc_measurement_index: Union[int, None] = None
2838
- ) -> list[float]:
2839
- """
2840
- Scale soil organic carbon (SOC) stocks based on a measurement value and index.
2841
-
2842
- Parameters
2843
- ----------
2844
- soc_stocks : list[float]
2845
- The list of SOC stocks to be scaled.
2846
- soc_measurement_value : float | None, optional
2847
- The measured SOC value for scaling. If None, no scaling is applied.
2848
- soc_measurement_index : int | None, optional
2849
- The index of the calculated SOC stock to compare against the SOC measurement.
2850
-
2851
- Returns
2852
- -------
2853
- list[float]
2854
- The scaled SOC stocks.
2855
- """
2856
-
2857
- measurement_scaling_factor = (
2858
- _calc_measurement_scaling_factor(
2859
- soc_measurement_value,
2860
- soc_stocks[soc_measurement_index]
2861
- ) if soc_measurement_value and soc_measurement_index is not None
2862
- else 1
2863
- )
2864
-
2865
- return [value * measurement_scaling_factor for value in soc_stocks]
2866
-
2867
-
2868
2347
  # --- GET THE ECO-CLIMATE ZONE FROM THE MEASUREMENTS ---
2869
2348
 
2870
2349
 
@@ -2883,7 +2362,6 @@ def _get_eco_climate_zone(measurements: list[dict]) -> Optional[int]:
2883
2362
  The eco-climate zone value if found, otherwise None.
2884
2363
  """
2885
2364
  eco_climate_zone = find_term_match(measurements, "ecoClimateZone")
2886
- # return measurement_value(eco_climate_zone) or None
2887
2365
  return get_node_value(eco_climate_zone) or None
2888
2366
 
2889
2367
 
@@ -2891,10 +2369,10 @@ def _get_eco_climate_zone(measurements: list[dict]) -> Optional[int]:
2891
2369
 
2892
2370
 
2893
2371
  def _check_soil_category(
2894
- soil_types: list[dict],
2895
- usda_soil_types: list[dict],
2896
2372
  *,
2897
2373
  key: IpccSoilCategory,
2374
+ soil_types: list[dict],
2375
+ usda_soil_types: list[dict],
2898
2376
  **_
2899
2377
  ) -> bool:
2900
2378
  """
@@ -2902,12 +2380,12 @@ def _check_soil_category(
2902
2380
 
2903
2381
  Parameters
2904
2382
  ----------
2383
+ key : IpccSoilCategory
2384
+ The IPCC soil category to check.
2905
2385
  soil_types : list[dict]
2906
2386
  List of soil type measurement nodes.
2907
2387
  usda_soil_types : list[dict]
2908
2388
  List of USDA soil type measurement nodes
2909
- key : IpccSoilCategory
2910
- The IPCC soil category to check.
2911
2389
 
2912
2390
  Returns
2913
2391
  -------
@@ -2923,26 +2401,25 @@ def _check_soil_category(
2923
2401
  soil_types,
2924
2402
  lookup=SOIL_TYPE_LOOKUP,
2925
2403
  target_lookup_values=target_lookup_values,
2926
- cumulative_threshold=MIN_AREA_THRESHOLD,
2927
- default_node_value=DEFAULT_NODE_VALUE
2404
+ cumulative_threshold=MIN_AREA_THRESHOLD
2928
2405
  )
2929
2406
 
2930
2407
  is_usda_soil_type_match = cumulative_nodes_lookup_match(
2931
2408
  usda_soil_types,
2932
2409
  lookup=USDA_SOIL_TYPE_LOOKUP,
2933
2410
  target_lookup_values=target_lookup_values,
2934
- cumulative_threshold=MIN_AREA_THRESHOLD,
2935
- default_node_value=DEFAULT_NODE_VALUE
2411
+ cumulative_threshold=MIN_AREA_THRESHOLD
2936
2412
  )
2937
2413
 
2938
2414
  return is_soil_type_match or is_usda_soil_type_match
2939
2415
 
2940
2416
 
2941
- def _check_sandy_soils(
2417
+ def _check_sandy_soil_category(
2418
+ *,
2419
+ key: IpccSoilCategory,
2942
2420
  soil_types: list[dict],
2943
2421
  usda_soil_types: list[dict],
2944
- *,
2945
- is_sandy: bool,
2422
+ has_sandy_soil: bool,
2946
2423
  **_
2947
2424
  ) -> bool:
2948
2425
  """
@@ -2952,11 +2429,13 @@ def _check_sandy_soils(
2952
2429
 
2953
2430
  Parameters
2954
2431
  ----------
2432
+ key : IpccSoilCategory
2433
+ The IPCC soil category to check.
2955
2434
  soil_types : list[dict]
2956
2435
  List of soil type measurement nodes.
2957
2436
  usda_soil_types : list[dict]
2958
2437
  List of USDA soil type measurement nodes
2959
- is_sandy : bool
2438
+ has_sandy_soil : bool
2960
2439
  True if the soils are sandy, False otherwise.
2961
2440
 
2962
2441
  Returns
@@ -2964,13 +2443,12 @@ def _check_sandy_soils(
2964
2443
  bool
2965
2444
  `True` if the soil category matches, `False` otherwise.
2966
2445
  """
2967
- KEY = IpccSoilCategory.SANDY_SOILS
2968
- return _check_soil_category(soil_types, usda_soil_types, key=KEY) or is_sandy
2446
+ return _check_soil_category(key=key, soil_types=soil_types, usda_soil_types=usda_soil_types) or has_sandy_soil
2969
2447
 
2970
2448
 
2971
2449
  SOIL_CATEGORY_DECISION_TREE = {
2972
2450
  IpccSoilCategory.ORGANIC_SOILS: _check_soil_category,
2973
- IpccSoilCategory.SANDY_SOILS: _check_sandy_soils,
2451
+ IpccSoilCategory.SANDY_SOILS: _check_sandy_soil_category,
2974
2452
  IpccSoilCategory.WETLAND_SOILS: _check_soil_category,
2975
2453
  IpccSoilCategory.VOLCANIC_SOILS: _check_soil_category,
2976
2454
  IpccSoilCategory.SPODIC_SOILS: _check_soil_category,
@@ -3009,19 +2487,17 @@ def _assign_ipcc_soil_category(
3009
2487
 
3010
2488
  clay_content = get_node_value(find_term_match(measurements, CLAY_CONTENT_TERM_ID))
3011
2489
  sand_content = get_node_value(find_term_match(measurements, SAND_CONTENT_TERM_ID))
3012
- # clay_content = measurement_value(find_term_match(measurements, CLAY_CONTENT_TERM_ID))
3013
- # sand_content = measurement_value(find_term_match(measurements, SAND_CONTENT_TERM_ID))
3014
2490
 
3015
- is_sandy = clay_content < CLAY_CONTENT_MAX and sand_content > SAND_CONTENT_MIN
2491
+ has_sandy_soil = clay_content < CLAY_CONTENT_MAX and sand_content > SAND_CONTENT_MIN
3016
2492
 
3017
2493
  return next(
3018
2494
  (
3019
2495
  key for key in SOIL_CATEGORY_DECISION_TREE
3020
2496
  if SOIL_CATEGORY_DECISION_TREE[key](
3021
- soil_types,
3022
- usda_soil_types,
3023
2497
  key=key,
3024
- is_sandy=is_sandy
2498
+ soil_types=soil_types,
2499
+ usda_soil_types=usda_soil_types,
2500
+ has_sandy_soil=has_sandy_soil
3025
2501
  )
3026
2502
  ),
3027
2503
  default
@@ -3048,182 +2524,174 @@ def _has_irrigation(water_regime_nodes: list[dict]) -> bool:
3048
2524
  return cumulative_nodes_term_match(
3049
2525
  water_regime_nodes,
3050
2526
  target_term_ids=get_irrigated_terms(),
3051
- cumulative_threshold=MIN_AREA_THRESHOLD,
3052
- default_node_value=DEFAULT_NODE_VALUE
2527
+ cumulative_threshold=MIN_AREA_THRESHOLD
3053
2528
  )
3054
2529
 
3055
2530
 
3056
- def _check_ipcc_land_use_category(
3057
- site_type: str,
3058
- *,
3059
- key: IpccLandUseCategory,
3060
- **_
3061
- ):
2531
+ def _has_long_fallow(land_cover_nodes: list[dict]) -> bool:
3062
2532
  """
3063
- Check if the site type matches the target site type for the given key.
2533
+ Check if long fallow terms is present in the land cover nodes.
2534
+
2535
+ n.b., a super majority of the site area must be under long fallow for it to be classified as set aside.
3064
2536
 
3065
2537
  Parameters
3066
2538
  ----------
3067
- site_type : str
3068
- The site type to check.
3069
- key : IpccLandUseCategory
3070
- The IPCC land use category to check.
2539
+ land_cover_nodes : List[dict]
2540
+ List of land cover nodes to be checked.
3071
2541
 
3072
2542
  Returns
3073
2543
  -------
3074
2544
  bool
3075
- `True` if the conditions match the specified land use category, `False` otherwise.
3076
-
2545
+ `True` if long fallow is present, `False` otherwise.
3077
2546
  """
3078
- target_site_type = IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE.get(key, None)
3079
-
3080
- return site_type == target_site_type
2547
+ return cumulative_nodes_term_match(
2548
+ land_cover_nodes,
2549
+ target_term_ids=get_long_fallow_land_cover_terms(),
2550
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
2551
+ ) or cumulative_nodes_match(
2552
+ lambda node: get_node_property(node, LONG_FALLOW_CROP_TERM_ID, False).get("value", 0),
2553
+ land_cover_nodes,
2554
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
2555
+ )
3081
2556
 
3082
2557
 
3083
- def _check_cropland_land_use_category(
3084
- site_type: str,
3085
- *,
3086
- key: IpccLandUseCategory,
3087
- land_cover_nodes: list[dict],
3088
- **_
3089
- ) -> bool:
2558
+ def _has_upland_rice(land_cover_nodes: list[dict]) -> bool:
3090
2559
  """
3091
- Check if the site type and land cover nodes match the target conditions for a cropland IpccLandUseCategory.
3092
-
3093
- This function is special case of `_check_cropland_land_use_category`.
2560
+ Check if upland rice is present in the land cover nodes.
3094
2561
 
3095
2562
  Parameters
3096
2563
  ----------
3097
- site_type : str
3098
- The site type to check.
3099
- key : IpccLandUseCategory
3100
- The IPCC land use category to check.
3101
- land_cover_nodes : list[dict]
3102
- List of land cover nodes.
2564
+ land_cover_nodes : List[dict]
2565
+ List of land cover nodes to be checked.
3103
2566
 
3104
2567
  Returns
3105
2568
  -------
3106
2569
  bool
3107
- `True` if the conditions match the specified land use category, `False` otherwise.
2570
+ `True` if upland rice is present, `False` otherwise.
3108
2571
  """
3109
- LOOKUP = LOOKUPS["landCover"][0]
3110
- target_lookup_values = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(key, None)
3111
-
3112
- return _check_ipcc_land_use_category(site_type, key=key) and cumulative_nodes_lookup_match(
2572
+ return cumulative_nodes_term_match(
3113
2573
  land_cover_nodes,
3114
- lookup=LOOKUP,
3115
- target_lookup_values=target_lookup_values,
3116
- cumulative_threshold=MIN_AREA_THRESHOLD,
3117
- default_node_value=DEFAULT_NODE_VALUE
2574
+ target_term_ids=get_upland_rice_land_cover_terms(),
2575
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
3118
2576
  )
3119
2577
 
3120
2578
 
3121
- def _check_paddy_rice_cultivation_category(
3122
- site_type: str,
3123
- *,
3124
- land_cover_nodes: list[dict],
3125
- is_irrigated_upland_rice: bool,
3126
- **_
3127
- ) -> bool:
3128
- """
3129
- Check if the site type and land cover nodes match the target conditions for paddy rice cultivation.
2579
+ IPCC_LAND_USE_CATEGORY_TO_VALIDATION_KWARGS = {
2580
+ IpccLandUseCategory.SET_ASIDE: {"has_long_fallow"},
2581
+ IpccLandUseCategory.ANNUAL_CROPS_WET: {"has_wetland_soils"}
2582
+ }
2583
+ """
2584
+ Keyword arguments that need to be checked for specific `IpccLandUseCategory`s.
2585
+ """
3130
2586
 
3131
- This function is special case of `_check_cropland_land_use_category`.
2587
+
2588
+ def _check_ipcc_land_use_category(*, key: IpccLandUseCategory, site_type: str, **kwargs) -> bool:
2589
+ """
2590
+ Check if the site type matches the target site type for the given key.
3132
2591
 
3133
2592
  Parameters
3134
2593
  ----------
2594
+ key : IpccLandUseCategory
2595
+ The IPCC land use category to check.
3135
2596
  site_type : str
3136
2597
  The site type to check.
3137
- land_cover_nodes : list[dict]
3138
- List of land cover nodes.
3139
- is_irrigated_upland_rice : bool
3140
- Flag indicating if the land cover irrigated upland rice.
2598
+
2599
+ Keyword Args
2600
+ ------------
2601
+ has_long_fallow : bool
2602
+ Indicates whether long fallow is present on more than 30% of the site.
2603
+ has_wetland_soils : bool
2604
+ Indicates whether wetland soils are present to more than 30% of the site.
3141
2605
 
3142
2606
  Returns
3143
2607
  -------
3144
2608
  bool
3145
2609
  `True` if the conditions match the specified land use category, `False` otherwise.
2610
+
3146
2611
  """
3147
- KEY = IpccLandUseCategory.PADDY_RICE_CULTIVATION
3148
- return _check_cropland_land_use_category(
3149
- site_type, key=KEY, land_cover_nodes=land_cover_nodes
3150
- ) or is_irrigated_upland_rice
2612
+ target_site_type = IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE.get(key, None)
2613
+ validation_kwargs = IPCC_LAND_USE_CATEGORY_TO_VALIDATION_KWARGS.get(key, set())
2614
+ valid_kwargs = all(v for k, v in kwargs.items() if k in validation_kwargs)
2615
+ return site_type == target_site_type and valid_kwargs
3151
2616
 
3152
2617
 
3153
- def _check_annual_crops_category(
3154
- site_type: str,
3155
- *,
3156
- land_cover_nodes: list[dict],
3157
- has_long_fallow: bool,
3158
- **_
2618
+ def _check_cropland_land_use_category(
2619
+ *, key: IpccLandUseCategory, site_type: str, land_cover_nodes: list[dict], **kwargs
3159
2620
  ) -> bool:
3160
2621
  """
3161
- Check if the site type and land cover nodes match the target conditions for annual crops.
2622
+ Check if the site type and land cover nodes match the target conditions for a cropland IpccLandUseCategory.
3162
2623
 
3163
- This function is special case of `_check_cropland_land_use_category`.
2624
+ This function is special case of `_check_ipcc_land_use_category`.
3164
2625
 
3165
2626
  Parameters
3166
2627
  ----------
2628
+ key : IpccLandUseCategory
2629
+ The IPCC land use category to check.
3167
2630
  site_type : str
3168
2631
  The site type to check.
3169
- land_cover_nodes : list[dict]
3170
- List of land cover nodes.
2632
+
2633
+ Keyword Args
2634
+ ------------
3171
2635
  has_long_fallow : bool
3172
- Flag indicating if there is long fallow.
2636
+ Indicates whether long fallow is present on more than 30% of the site.
2637
+ has_wetland_soils : bool
2638
+ Indicates whether wetland soils are present to more than 30% of the site.
3173
2639
 
3174
2640
  Returns
3175
2641
  -------
3176
2642
  bool
3177
2643
  `True` if the conditions match the specified land use category, `False` otherwise.
3178
2644
  """
3179
- KEY = IpccLandUseCategory.ANNUAL_CROPS
3180
- return _check_cropland_land_use_category(
3181
- site_type, key=KEY, land_cover_nodes=land_cover_nodes
3182
- ) and not has_long_fallow
2645
+ LOOKUP = LOOKUPS["landCover"][0]
2646
+ target_lookup_values = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(key, None)
2647
+ valid_lookup = cumulative_nodes_lookup_match(
2648
+ land_cover_nodes,
2649
+ lookup=LOOKUP,
2650
+ target_lookup_values=target_lookup_values,
2651
+ cumulative_threshold=MIN_AREA_THRESHOLD
2652
+ )
2653
+ return _check_ipcc_land_use_category(key=key, site_type=site_type, **kwargs) and valid_lookup
3183
2654
 
3184
2655
 
3185
- def _check_annual_crops_wet_category(
3186
- site_type: str,
3187
- *,
3188
- land_cover_nodes: list[dict],
3189
- has_wetland_soils: bool,
3190
- has_long_fallow: bool,
3191
- **_
3192
- ):
2656
+ def _check_paddy_rice_cultivation_land_use_category(
2657
+ *, key: IpccLandUseCategory, site_type: str, has_irrigated_upland_rice: bool, **kwargs
2658
+ ) -> bool:
3193
2659
  """
3194
- Check if the site type and land cover nodes match the target conditions for annual crops on wetland soils.
2660
+ Check if the site type and land cover nodes match the target conditions for a cropland IpccLandUseCategory.
3195
2661
 
3196
2662
  This function is special case of `_check_cropland_land_use_category`.
3197
2663
 
3198
2664
  Parameters
3199
2665
  ----------
2666
+ key : IpccLandUseCategory
2667
+ The IPCC land use category to check.
3200
2668
  site_type : str
3201
2669
  The site type to check.
3202
- land_cover_nodes : list[dict]
3203
- List of land cover nodes.
3204
- has_wetland_soils : bool
3205
- Flag indicating if the site is classified as `IpccSoilCategory.WETLAND_SOILS`
2670
+
2671
+ Keyword Args
2672
+ ------------
2673
+ has_irrigated_upland_rice : bool
2674
+ Indicates whether irrigated upland rice is present on more than 30% of the site.
3206
2675
  has_long_fallow : bool
3207
- Flag indicating if there is long fallow.
2676
+ Indicates whether long fallow is present on more than 30% of the site.
2677
+ has_wetland_soils : bool
2678
+ Indicates whether wetland soils are present to more than 30% of the site.
3208
2679
 
3209
2680
  Returns
3210
2681
  -------
3211
2682
  bool
3212
2683
  `True` if the conditions match the specified land use category, `False` otherwise.
3213
2684
  """
3214
- KEY = IpccLandUseCategory.ANNUAL_CROPS_WET
3215
- return _check_cropland_land_use_category(
3216
- site_type, key=KEY, land_cover_nodes=land_cover_nodes
3217
- ) and has_wetland_soils and not has_long_fallow
2685
+ return _check_cropland_land_use_category(key=key, site_type=site_type, **kwargs) or has_irrigated_upland_rice
3218
2686
 
3219
2687
 
3220
2688
  LAND_USE_CATEGORY_DECISION_TREE = {
3221
2689
  IpccLandUseCategory.GRASSLAND: _check_ipcc_land_use_category,
3222
- IpccLandUseCategory.PERENNIAL_CROPS: _check_cropland_land_use_category,
3223
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: _check_paddy_rice_cultivation_category,
3224
- IpccLandUseCategory.ANNUAL_CROPS_WET: _check_annual_crops_wet_category,
3225
- IpccLandUseCategory.ANNUAL_CROPS: _check_annual_crops_category,
3226
2690
  IpccLandUseCategory.SET_ASIDE: _check_ipcc_land_use_category,
2691
+ IpccLandUseCategory.PERENNIAL_CROPS: _check_cropland_land_use_category,
2692
+ IpccLandUseCategory.PADDY_RICE_CULTIVATION: _check_paddy_rice_cultivation_land_use_category,
2693
+ IpccLandUseCategory.ANNUAL_CROPS_WET: _check_cropland_land_use_category,
2694
+ IpccLandUseCategory.ANNUAL_CROPS: _check_cropland_land_use_category,
3227
2695
  IpccLandUseCategory.FOREST: _check_ipcc_land_use_category,
3228
2696
  IpccLandUseCategory.NATIVE: _check_ipcc_land_use_category,
3229
2697
  IpccLandUseCategory.OTHER: _check_ipcc_land_use_category
@@ -3238,9 +2706,7 @@ and land cover nodes.
3238
2706
 
3239
2707
 
3240
2708
  def _assign_ipcc_land_use_category(
3241
- site_type: str,
3242
- management_nodes: list[dict],
3243
- ipcc_soil_category: IpccSoilCategory
2709
+ site_type: str, management_nodes: list[dict], ipcc_soil_category: IpccSoilCategory
3244
2710
  ) -> IpccLandUseCategory:
3245
2711
  """
3246
2712
  Assigns IPCC land use category based on site type, management nodes, and soil category.
@@ -3262,33 +2728,13 @@ def _assign_ipcc_land_use_category(
3262
2728
  DECISION_TREE = LAND_USE_CATEGORY_DECISION_TREE
3263
2729
  DEFAULT = IpccLandUseCategory.OTHER
3264
2730
 
3265
- land_cover_nodes = filter_list_term_type(
3266
- management_nodes, [TermTermType.LANDCOVER]
3267
- )
3268
-
3269
- water_regime_nodes = filter_list_term_type(
3270
- management_nodes, [TermTermType.WATERREGIME]
3271
- )
2731
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
2732
+ water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
3272
2733
 
3273
2734
  has_irrigation = _has_irrigation(water_regime_nodes)
3274
-
3275
- is_upland_rice = cumulative_nodes_term_match(
3276
- land_cover_nodes,
3277
- target_term_ids=get_rice_plant_upland_terms(),
3278
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD,
3279
- default_node_value=DEFAULT_NODE_VALUE
3280
- )
3281
-
3282
- is_irrigated_upland_rice = is_upland_rice and has_irrigation
3283
-
3284
- # SUPER_MAJORITY_AREA_THRESHOLD
3285
- has_long_fallow = cumulative_nodes_match(
3286
- lambda node: get_node_property(node, LONG_FALLOW_CROP_TERM_ID, False).get("value", 0),
3287
- land_cover_nodes,
3288
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD,
3289
- default_node_value=DEFAULT_NODE_VALUE
3290
- )
3291
-
2735
+ has_upland_rice = _has_upland_rice(land_cover_nodes)
2736
+ has_irrigated_upland_rice = has_upland_rice and has_irrigation
2737
+ has_long_fallow = _has_long_fallow(land_cover_nodes)
3292
2738
  has_wetland_soils = ipcc_soil_category is IpccSoilCategory.WETLAND_SOILS
3293
2739
 
3294
2740
  should_run = bool(site_type)
@@ -3297,11 +2743,11 @@ def _assign_ipcc_land_use_category(
3297
2743
  (
3298
2744
  key for key in DECISION_TREE
3299
2745
  if DECISION_TREE[key](
3300
- site_type,
3301
2746
  key=key,
2747
+ site_type=site_type,
3302
2748
  land_cover_nodes=land_cover_nodes,
3303
- is_irrigated_upland_rice=is_irrigated_upland_rice,
3304
2749
  has_long_fallow=has_long_fallow,
2750
+ has_irrigated_upland_rice=has_irrigated_upland_rice,
3305
2751
  has_wetland_soils=has_wetland_soils
3306
2752
  )
3307
2753
  ),
@@ -3313,20 +2759,17 @@ def _assign_ipcc_land_use_category(
3313
2759
 
3314
2760
 
3315
2761
  def _check_grassland_ipcc_management_category(
3316
- *,
3317
- land_cover_nodes: list[dict],
3318
- key: IpccManagementCategory,
3319
- **_
2762
+ *, key: IpccManagementCategory, land_cover_nodes: list[dict], **_
3320
2763
  ) -> bool:
3321
2764
  """
3322
2765
  Check if the land cover nodes match the target conditions for a grassland IpccManagementCategory.
3323
2766
 
3324
2767
  Parameters
3325
2768
  ----------
3326
- land_cover_nodes : List[dict]
3327
- List of land cover nodes to be checked.
3328
2769
  key : IpccManagementCategory
3329
2770
  The IPCC management category to check.
2771
+ land_cover_nodes : List[dict]
2772
+ List of land cover nodes to be checked.
3330
2773
 
3331
2774
  Returns
3332
2775
  -------
@@ -3334,25 +2777,25 @@ def _check_grassland_ipcc_management_category(
3334
2777
  `True` if the conditions match the specified management category, `False` otherwise.
3335
2778
  """
3336
2779
  target_term_id = IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID.get(key, None)
3337
-
3338
2780
  return cumulative_nodes_term_match(
3339
2781
  land_cover_nodes,
3340
2782
  target_term_ids=target_term_id,
3341
- cumulative_threshold=MIN_AREA_THRESHOLD,
3342
- default_node_value=DEFAULT_NODE_VALUE
2783
+ cumulative_threshold=MIN_AREA_THRESHOLD
3343
2784
  )
3344
2785
 
3345
2786
 
3346
- def _check_tillage_ipcc_management_category(*, tillage_nodes: list[dict], key: IpccManagementCategory, **_) -> bool:
2787
+ def _check_tillage_ipcc_management_category(
2788
+ *, key: IpccManagementCategory, tillage_nodes: list[dict], **_
2789
+ ) -> bool:
3347
2790
  """
3348
2791
  Check if the tillage nodes match the target conditions for a tillage IpccManagementCategory.
3349
2792
 
3350
2793
  Parameters
3351
2794
  ----------
3352
- tillage_nodes : List[dict]
3353
- List of tillage nodes to be checked.
3354
2795
  key : IpccManagementCategory
3355
2796
  The IPCC management category to check.
2797
+ tillage_nodes : List[dict]
2798
+ List of tillage nodes to be checked.
3356
2799
 
3357
2800
  Returns
3358
2801
  -------
@@ -3361,13 +2804,11 @@ def _check_tillage_ipcc_management_category(*, tillage_nodes: list[dict], key: I
3361
2804
  """
3362
2805
  LOOKUP = LOOKUPS["tillage"]
3363
2806
  target_lookup_values = IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE.get(key, None)
3364
-
3365
2807
  return cumulative_nodes_lookup_match(
3366
2808
  tillage_nodes,
3367
2809
  lookup=LOOKUP,
3368
2810
  target_lookup_values=target_lookup_values,
3369
- cumulative_threshold=MIN_AREA_THRESHOLD,
3370
- default_node_value=DEFAULT_NODE_VALUE
2811
+ cumulative_threshold=MIN_AREA_THRESHOLD
3371
2812
  )
3372
2813
 
3373
2814
 
@@ -3423,8 +2864,7 @@ Value: Default IPCC management category for the given land use category.
3423
2864
 
3424
2865
 
3425
2866
  def _assign_ipcc_management_category(
3426
- management_nodes: list[dict],
3427
- ipcc_land_use_category: IpccLandUseCategory
2867
+ management_nodes: list[dict], ipcc_land_use_category: IpccLandUseCategory
3428
2868
  ) -> IpccManagementCategory:
3429
2869
  """
3430
2870
  Assign an IPCC Management Category based on the given management nodes and IPCC Land Use Category.
@@ -3441,32 +2881,26 @@ def _assign_ipcc_management_category(
3441
2881
  IpccManagementCategory
3442
2882
  The assigned IPCC Management Category.
3443
2883
  """
3444
- decision_tree = IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE.get(
3445
- ipcc_land_use_category, {}
3446
- )
2884
+ decision_tree = IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE.get(ipcc_land_use_category, {})
3447
2885
  default = IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY.get(
3448
2886
  ipcc_land_use_category, IpccManagementCategory.OTHER
3449
2887
  )
3450
2888
 
3451
- land_cover_nodes = filter_list_term_type(
3452
- management_nodes, [TermTermType.LANDCOVER]
3453
- )
3454
- tillage_nodes = filter_list_term_type(
3455
- management_nodes, [TermTermType.TILLAGE]
3456
- )
2889
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
2890
+ tillage_nodes = filter_list_term_type(management_nodes, [TermTermType.TILLAGE])
3457
2891
 
3458
- should_run = (
3459
- len(land_cover_nodes) > 0 if decision_tree == GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE
3460
- else len(tillage_nodes) > 0
3461
- )
2892
+ should_run = any([
2893
+ decision_tree == GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE and len(land_cover_nodes) > 0,
2894
+ decision_tree == TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE and len(tillage_nodes) > 0
2895
+ ])
3462
2896
 
3463
2897
  return next(
3464
2898
  (
3465
2899
  key for key in decision_tree
3466
2900
  if decision_tree[key](
2901
+ key=key,
3467
2902
  land_cover_nodes=land_cover_nodes,
3468
2903
  tillage_nodes=tillage_nodes,
3469
- key=key
3470
2904
  )
3471
2905
  ),
3472
2906
  default
@@ -3489,9 +2923,7 @@ Value: Minimum number of improvements required for the corresponding Grassland C
3489
2923
 
3490
2924
 
3491
2925
  def _check_grassland_ipcc_carbon_input_category(
3492
- carbon_input_args: CarbonInputArgs,
3493
- *,
3494
- key: IpccCarbonInputCategory
2926
+ *, key: IpccCarbonInputCategory, num_grassland_improvements: int, **_,
3495
2927
  ) -> bool:
3496
2928
  """
3497
2929
  Checks if the given carbon input arguments satisfy the conditions for a specific
@@ -3499,24 +2931,26 @@ def _check_grassland_ipcc_carbon_input_category(
3499
2931
 
3500
2932
  Parameters
3501
2933
  ----------
3502
- carbon_input_args : CarbonInputArgs
3503
- The carbon input arguments.
3504
2934
  key : IpccCarbonInputCategory
3505
2935
  The grassland IPCC Carbon Input Category to check.
2936
+ num_grassland_improvements : int
2937
+ The number of grassland improvements.
3506
2938
 
3507
2939
  Returns
3508
2940
  -------
3509
2941
  bool
3510
2942
  `True` if the conditions for the specified category are met; otherwise, `False`.
3511
2943
  """
3512
- min_improvements = (
3513
- GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS[key]
3514
- )
3515
- return carbon_input_args.num_grassland_improvements >= min_improvements
2944
+ return num_grassland_improvements >= GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS[key]
3516
2945
 
3517
2946
 
3518
2947
  def _check_cropland_high_with_manure_category(
3519
- carbon_input_args: CarbonInputArgs,
2948
+ *,
2949
+ has_animal_manure_used: bool,
2950
+ has_bare_fallow: bool,
2951
+ has_low_residue_producing_crops: bool,
2952
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
2953
+ has_residue_removed_or_burnt: bool,
3520
2954
  **_
3521
2955
  ) -> Optional[int]:
3522
2956
  """
@@ -3524,8 +2958,16 @@ def _check_cropland_high_with_manure_category(
3524
2958
 
3525
2959
  Parameters
3526
2960
  ----------
3527
- carbon_input_args : CarbonInputArgs
3528
- The carbon input arguments.
2961
+ has_animal_manure_used : bool
2962
+ Indicates whether animal manure is used on more than 30% of the site.
2963
+ has_bare_fallow : bool
2964
+ Indicates whether bare fallow is present on more than 30% of the site.
2965
+ has_low_residue_producing_crops : bool
2966
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
2967
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
2968
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
2969
+ has_residue_removed_or_burnt : bool
2970
+ Indicates whether residues are removed or burnt on more than 30% of the site.
3529
2971
 
3530
2972
  Returns
3531
2973
  -------
@@ -3534,11 +2976,11 @@ def _check_cropland_high_with_manure_category(
3534
2976
  """
3535
2977
  conditions = {
3536
2978
  1: all([
3537
- not carbon_input_args.has_residue_removed_or_burnt,
3538
- not carbon_input_args.has_low_residue_producing_crops,
3539
- not carbon_input_args.has_bare_fallow,
3540
- carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3541
- carbon_input_args.has_animal_manure_used
2979
+ not has_residue_removed_or_burnt,
2980
+ not has_low_residue_producing_crops,
2981
+ not has_bare_fallow,
2982
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
2983
+ has_animal_manure_used
3542
2984
  ])
3543
2985
  }
3544
2986
 
@@ -3548,15 +2990,41 @@ def _check_cropland_high_with_manure_category(
3548
2990
 
3549
2991
 
3550
2992
  def _check_cropland_high_without_manure_category(
3551
- carbon_input_args: CarbonInputArgs, **_
2993
+ *,
2994
+ has_animal_manure_used: bool,
2995
+ has_bare_fallow: bool,
2996
+ has_cover_crop: bool,
2997
+ has_irrigation: bool,
2998
+ has_low_residue_producing_crops: bool,
2999
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
3000
+ has_organic_fertiliser_or_soil_amendment_used: bool,
3001
+ has_practice_increasing_c_input: bool,
3002
+ has_residue_removed_or_burnt: bool,
3003
+ **_
3552
3004
  ) -> Optional[int]:
3553
3005
  """
3554
3006
  Checks the Cropland High without Manure IPCC Carbon Input Category based on the given carbon input arguments.
3555
3007
 
3556
3008
  Parameters
3557
3009
  ----------
3558
- carbon_input_args : CarbonInputArgs
3559
- The carbon input arguments.
3010
+ has_animal_manure_used : bool
3011
+ Indicates whether animal manure is used on more than 30% of the site.
3012
+ has_bare_fallow : bool
3013
+ Indicates whether bare fallow is present on more than 30% of the site.
3014
+ has_cover_crop : bool
3015
+ Indicates whether cover crops are present on more than 30% of the site.
3016
+ has_irrigation : bool
3017
+ Indicates whether irrigation is applied to more than 30% of the site.
3018
+ has_low_residue_producing_crops : bool
3019
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
3020
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
3021
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
3022
+ has_organic_fertiliser_or_soil_amendment_used : bool
3023
+ Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
3024
+ has_practice_increasing_c_input : bool
3025
+ Indicates whether practices increasing carbon input are present on more than 30% of the site.
3026
+ has_residue_removed_or_burnt : bool
3027
+ Indicates whether residues are removed or burnt on more than 30% of the site.
3560
3028
 
3561
3029
  Returns
3562
3030
  -------
@@ -3565,17 +3033,17 @@ def _check_cropland_high_without_manure_category(
3565
3033
  """
3566
3034
  conditions = {
3567
3035
  1: all([
3568
- not carbon_input_args.has_residue_removed_or_burnt,
3569
- not carbon_input_args.has_low_residue_producing_crops,
3570
- not carbon_input_args.has_bare_fallow,
3571
- carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3036
+ not has_residue_removed_or_burnt,
3037
+ not has_low_residue_producing_crops,
3038
+ not has_bare_fallow,
3039
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3572
3040
  any([
3573
- carbon_input_args.has_irrigation,
3574
- carbon_input_args.has_practice_increasing_c_input,
3575
- carbon_input_args.has_cover_crop,
3576
- carbon_input_args.has_organic_fertiliser_or_soil_amendment_used
3041
+ has_irrigation,
3042
+ has_practice_increasing_c_input,
3043
+ has_cover_crop,
3044
+ has_organic_fertiliser_or_soil_amendment_used
3577
3045
  ]),
3578
- not carbon_input_args.has_animal_manure_used
3046
+ not has_animal_manure_used
3579
3047
  ])
3580
3048
  }
3581
3049
 
@@ -3585,7 +3053,16 @@ def _check_cropland_high_without_manure_category(
3585
3053
 
3586
3054
 
3587
3055
  def _check_cropland_medium_category(
3588
- carbon_input_args: CarbonInputArgs,
3056
+ *,
3057
+ has_animal_manure_used: bool,
3058
+ has_bare_fallow: bool,
3059
+ has_cover_crop: bool,
3060
+ has_irrigation: bool,
3061
+ has_low_residue_producing_crops: bool,
3062
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
3063
+ has_organic_fertiliser_or_soil_amendment_used: bool,
3064
+ has_practice_increasing_c_input: bool,
3065
+ has_residue_removed_or_burnt: bool,
3589
3066
  **_
3590
3067
  ) -> Optional[int]:
3591
3068
  """
@@ -3593,8 +3070,24 @@ def _check_cropland_medium_category(
3593
3070
 
3594
3071
  Parameters
3595
3072
  ----------
3596
- carbon_input_args : CarbonInputArgs
3597
- The carbon input arguments.
3073
+ has_animal_manure_used : bool
3074
+ Indicates whether animal manure is used on more than 30% of the site.
3075
+ has_bare_fallow : bool
3076
+ Indicates whether bare fallow is present on more than 30% of the site.
3077
+ has_cover_crop : bool
3078
+ Indicates whether cover crops are present on more than 30% of the site.
3079
+ has_irrigation : bool
3080
+ Indicates whether irrigation is applied to more than 30% of the site.
3081
+ has_low_residue_producing_crops : bool
3082
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
3083
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
3084
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
3085
+ has_organic_fertiliser_or_soil_amendment_used : bool
3086
+ Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
3087
+ has_practice_increasing_c_input : bool
3088
+ Indicates whether practices increasing carbon input are present on more than 30% of the site.
3089
+ has_residue_removed_or_burnt : bool
3090
+ Indicates whether residues are removed or burnt on more than 30% of the site.
3598
3091
 
3599
3092
  Returns
3600
3093
  -------
@@ -3603,43 +3096,43 @@ def _check_cropland_medium_category(
3603
3096
  """
3604
3097
  conditions = {
3605
3098
  1: all([
3606
- carbon_input_args.has_residue_removed_or_burnt,
3607
- carbon_input_args.has_animal_manure_used
3099
+ has_residue_removed_or_burnt,
3100
+ has_animal_manure_used
3608
3101
  ]),
3609
3102
  2: all([
3610
- not carbon_input_args.has_residue_removed_or_burnt,
3103
+ not has_residue_removed_or_burnt,
3611
3104
  any([
3612
- carbon_input_args.has_low_residue_producing_crops,
3613
- carbon_input_args.has_bare_fallow
3105
+ has_low_residue_producing_crops,
3106
+ has_bare_fallow
3614
3107
  ]),
3615
3108
  any([
3616
- carbon_input_args.has_irrigation,
3617
- carbon_input_args.has_practice_increasing_c_input,
3618
- carbon_input_args.has_cover_crop,
3619
- carbon_input_args.has_organic_fertiliser_or_soil_amendment_used,
3109
+ has_irrigation,
3110
+ has_practice_increasing_c_input,
3111
+ has_cover_crop,
3112
+ has_organic_fertiliser_or_soil_amendment_used,
3620
3113
  ])
3621
3114
  ]),
3622
3115
  3: all([
3623
- not carbon_input_args.has_residue_removed_or_burnt,
3624
- not carbon_input_args.has_low_residue_producing_crops,
3625
- not carbon_input_args.has_bare_fallow,
3626
- not carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3116
+ not has_residue_removed_or_burnt,
3117
+ not has_low_residue_producing_crops,
3118
+ not has_bare_fallow,
3119
+ not has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3627
3120
  any([
3628
- carbon_input_args.has_irrigation,
3629
- carbon_input_args.has_practice_increasing_c_input,
3630
- carbon_input_args.has_cover_crop,
3631
- carbon_input_args.has_organic_fertiliser_or_soil_amendment_used
3121
+ has_irrigation,
3122
+ has_practice_increasing_c_input,
3123
+ has_cover_crop,
3124
+ has_organic_fertiliser_or_soil_amendment_used
3632
3125
  ])
3633
3126
  ]),
3634
3127
  4: all([
3635
- not carbon_input_args.has_residue_removed_or_burnt,
3636
- not carbon_input_args.has_low_residue_producing_crops,
3637
- not carbon_input_args.has_bare_fallow,
3638
- carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3639
- not carbon_input_args.has_irrigation,
3640
- not carbon_input_args.has_organic_fertiliser_or_soil_amendment_used,
3641
- not carbon_input_args.has_practice_increasing_c_input,
3642
- not carbon_input_args.has_cover_crop
3128
+ not has_residue_removed_or_burnt,
3129
+ not has_low_residue_producing_crops,
3130
+ not has_bare_fallow,
3131
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3132
+ not has_irrigation,
3133
+ not has_organic_fertiliser_or_soil_amendment_used,
3134
+ not has_practice_increasing_c_input,
3135
+ not has_cover_crop
3643
3136
  ])
3644
3137
  }
3645
3138
 
@@ -3649,7 +3142,16 @@ def _check_cropland_medium_category(
3649
3142
 
3650
3143
 
3651
3144
  def _check_cropland_low_category(
3652
- carbon_input_args: CarbonInputArgs,
3145
+ *,
3146
+ has_animal_manure_used: bool,
3147
+ has_bare_fallow: bool,
3148
+ has_cover_crop: bool,
3149
+ has_irrigation: bool,
3150
+ has_low_residue_producing_crops: bool,
3151
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
3152
+ has_organic_fertiliser_or_soil_amendment_used: bool,
3153
+ has_practice_increasing_c_input: bool,
3154
+ has_residue_removed_or_burnt: bool,
3653
3155
  **_
3654
3156
  ) -> Optional[int]:
3655
3157
  """
@@ -3657,8 +3159,24 @@ def _check_cropland_low_category(
3657
3159
 
3658
3160
  Parameters
3659
3161
  ----------
3660
- carbon_input_args : CarbonInputArgs
3661
- The carbon input arguments.
3162
+ has_animal_manure_used : bool
3163
+ Indicates whether animal manure is used on more than 30% of the site.
3164
+ has_bare_fallow : bool
3165
+ Indicates whether bare fallow is present on more than 30% of the site.
3166
+ has_cover_crop : bool
3167
+ Indicates whether cover crops are present on more than 30% of the site.
3168
+ has_irrigation : bool
3169
+ Indicates whether irrigation is applied to more than 30% of the site.
3170
+ has_low_residue_producing_crops : bool
3171
+ Indicates whether low residue-producing crops are present on more than 70% of the site.
3172
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
3173
+ Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
3174
+ has_organic_fertiliser_or_soil_amendment_used : bool
3175
+ Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
3176
+ has_practice_increasing_c_input : bool
3177
+ Indicates whether practices increasing carbon input are present on more than 30% of the site.
3178
+ has_residue_removed_or_burnt : bool
3179
+ Indicates whether residues are removed or burnt on more than 30% of the site.
3662
3180
 
3663
3181
  Returns
3664
3182
  -------
@@ -3667,29 +3185,29 @@ def _check_cropland_low_category(
3667
3185
  """
3668
3186
  conditions = {
3669
3187
  1: all([
3670
- carbon_input_args.has_residue_removed_or_burnt,
3671
- not carbon_input_args.has_animal_manure_used
3188
+ has_residue_removed_or_burnt,
3189
+ not has_animal_manure_used
3672
3190
  ]),
3673
3191
  2: all([
3674
- not carbon_input_args.has_residue_removed_or_burnt,
3192
+ not has_residue_removed_or_burnt,
3675
3193
  any([
3676
- carbon_input_args.has_low_residue_producing_crops,
3677
- carbon_input_args.has_bare_fallow
3194
+ has_low_residue_producing_crops,
3195
+ has_bare_fallow
3678
3196
  ]),
3679
- not carbon_input_args.has_irrigation,
3680
- not carbon_input_args.has_practice_increasing_c_input,
3681
- not carbon_input_args.has_cover_crop,
3682
- not carbon_input_args.has_organic_fertiliser_or_soil_amendment_used
3197
+ not has_irrigation,
3198
+ not has_practice_increasing_c_input,
3199
+ not has_cover_crop,
3200
+ not has_organic_fertiliser_or_soil_amendment_used
3683
3201
  ]),
3684
3202
  3: all([
3685
- not carbon_input_args.has_residue_removed_or_burnt,
3686
- not carbon_input_args.has_low_residue_producing_crops,
3687
- not carbon_input_args.has_bare_fallow,
3688
- not carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3689
- not carbon_input_args.has_irrigation,
3690
- not carbon_input_args.has_organic_fertiliser_or_soil_amendment_used,
3691
- not carbon_input_args.has_practice_increasing_c_input,
3692
- not carbon_input_args.has_cover_crop
3203
+ not has_residue_removed_or_burnt,
3204
+ not has_low_residue_producing_crops,
3205
+ not has_bare_fallow,
3206
+ not has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3207
+ not has_irrigation,
3208
+ not has_organic_fertiliser_or_soil_amendment_used,
3209
+ not has_practice_increasing_c_input,
3210
+ not has_cover_crop
3693
3211
  ])
3694
3212
  }
3695
3213
 
@@ -3698,9 +3216,9 @@ def _check_cropland_low_category(
3698
3216
  )
3699
3217
 
3700
3218
 
3701
- def _make_carbon_input_args(
3219
+ def _get_carbon_input_kwargs(
3702
3220
  management_nodes: list[dict]
3703
- ) -> CarbonInputArgs:
3221
+ ) -> dict:
3704
3222
  """
3705
3223
  Creates CarbonInputArgs based on the provided list of management nodes.
3706
3224
 
@@ -3711,8 +3229,8 @@ def _make_carbon_input_args(
3711
3229
 
3712
3230
  Returns
3713
3231
  -------
3714
- CarbonInputArgs
3715
- The carbon input arguments.
3232
+ dict
3233
+ The carbon input keyword arguments.
3716
3234
  """
3717
3235
 
3718
3236
  PRACTICE_INCREASING_C_INPUT_LOOKUP = LOOKUPS["landUseManagement"]
@@ -3727,62 +3245,56 @@ def _make_carbon_input_args(
3727
3245
  ORGANIC_FERTILISER_USED_TERM_ID
3728
3246
  }
3729
3247
 
3730
- crop_residue_management_nodes = filter_list_term_type(
3731
- management_nodes, [TermTermType.CROPRESIDUEMANAGEMENT]
3248
+ crop_residue_management_nodes = filter_list_term_type(management_nodes, [TermTermType.CROPRESIDUEMANAGEMENT])
3249
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
3250
+ land_use_management_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDUSEMANAGEMENT])
3251
+ water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
3252
+
3253
+ has_animal_manure_used = any(
3254
+ get_node_value(node) for node in land_use_management_nodes if node_term_match(node, ANIMAL_MANURE_USED_TERM_ID)
3732
3255
  )
3733
3256
 
3734
- land_cover_nodes = filter_list_term_type(
3735
- management_nodes, [TermTermType.LANDCOVER]
3257
+ has_bare_fallow = cumulative_nodes_term_match(
3258
+ land_cover_nodes,
3259
+ target_term_ids=SHORT_BARE_FALLOW_TERM_ID,
3260
+ cumulative_threshold=MIN_AREA_THRESHOLD
3736
3261
  )
3737
3262
 
3738
- land_use_management_nodes = filter_list_term_type(
3739
- management_nodes, [TermTermType.LANDUSEMANAGEMENT]
3263
+ has_cover_crop = cumulative_nodes_match(
3264
+ lambda node: any(
3265
+ get_node_property(node, term_id, False).get("value", False) for term_id in get_cover_crop_property_terms()
3266
+ ),
3267
+ land_cover_nodes,
3268
+ cumulative_threshold=MIN_AREA_THRESHOLD
3740
3269
  )
3741
3270
 
3742
- water_regime_nodes = filter_list_term_type(
3743
- management_nodes, [TermTermType.WATERREGIME]
3271
+ has_inorganic_n_fertiliser_used = any(
3272
+ get_node_value(node) for node in land_use_management_nodes
3273
+ if node_term_match(node, INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID)
3744
3274
  )
3745
3275
 
3746
3276
  has_irrigation = _has_irrigation(water_regime_nodes)
3747
3277
 
3748
- has_residue_removed_or_burnt = cumulative_nodes_term_match(
3749
- crop_residue_management_nodes,
3750
- target_term_ids=get_residue_removed_or_burnt_terms(),
3751
- cumulative_threshold=MIN_AREA_THRESHOLD,
3752
- default_node_value=DEFAULT_NODE_VALUE
3753
- )
3754
-
3755
3278
  # SUPER_MAJORITY_AREA_THRESHOLD
3756
3279
  has_low_residue_producing_crops = cumulative_nodes_lookup_match(
3757
3280
  land_cover_nodes,
3758
3281
  lookup=LOW_RESIDUE_PRODUCING_CROP_LOOKUP,
3759
3282
  target_lookup_values=True,
3760
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD,
3761
- default_node_value=DEFAULT_NODE_VALUE
3762
- )
3763
-
3764
- has_bare_fallow = cumulative_nodes_term_match(
3765
- land_use_management_nodes,
3766
- target_term_ids=SHORT_BARE_FALLOW_TERM_ID,
3767
- cumulative_threshold=MIN_AREA_THRESHOLD,
3768
- default_node_value=DEFAULT_NODE_VALUE
3283
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
3769
3284
  )
3770
3285
 
3771
3286
  has_n_fixing_crop = cumulative_nodes_lookup_match(
3772
3287
  land_cover_nodes,
3773
3288
  lookup=N_FIXING_CROP_LOOKUP,
3774
3289
  target_lookup_values=True,
3775
- cumulative_threshold=MIN_AREA_THRESHOLD,
3776
- default_node_value=DEFAULT_NODE_VALUE
3290
+ cumulative_threshold=MIN_AREA_THRESHOLD
3777
3291
  )
3778
3292
 
3779
- has_inorganic_n_fertiliser_used = any(
3780
- get_node_value(node) for node in land_use_management_nodes
3781
- if node_term_match(node, INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID)
3782
- )
3293
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used = has_n_fixing_crop or has_inorganic_n_fertiliser_used
3783
3294
 
3784
- has_n_fixing_crop_or_inorganic_n_fertiliser_used = (
3785
- has_n_fixing_crop or has_inorganic_n_fertiliser_used
3295
+ has_organic_fertiliser_or_soil_amendment_used = any(
3296
+ get_node_value(node) for node in land_use_management_nodes
3297
+ if node_term_match(node, ORGANIC_FERTILISER_USED_TERM_ID)
3786
3298
  )
3787
3299
 
3788
3300
  has_practice_increasing_c_input = cumulative_nodes_match(
@@ -3791,27 +3303,13 @@ def _make_carbon_input_args(
3791
3303
  and not node_term_match(node, EXCLUDED_PRACTICE_TERM_IDS)
3792
3304
  ),
3793
3305
  land_use_management_nodes,
3794
- cumulative_threshold=MIN_AREA_THRESHOLD,
3795
- default_node_value=DEFAULT_NODE_VALUE
3306
+ cumulative_threshold=MIN_AREA_THRESHOLD
3796
3307
  )
3797
3308
 
3798
- has_cover_crop = cumulative_nodes_match(
3799
- lambda node: any(
3800
- get_node_property(node, term_id, False).get("value", False) for term_id in get_cover_crop_property_terms()
3801
- ),
3802
- land_cover_nodes,
3803
- cumulative_threshold=MIN_AREA_THRESHOLD,
3804
- default_node_value=DEFAULT_NODE_VALUE
3805
- )
3806
-
3807
- has_organic_fertiliser_or_soil_amendment_used = any(
3808
- get_node_value(node) for node in land_use_management_nodes
3809
- if node_term_match(node, ORGANIC_FERTILISER_USED_TERM_ID)
3810
- )
3811
-
3812
- has_animal_manure_used = any(
3813
- get_node_value(node) for node in land_use_management_nodes
3814
- if node_term_match(node, ANIMAL_MANURE_USED_TERM_ID)
3309
+ has_residue_removed_or_burnt = cumulative_nodes_term_match(
3310
+ crop_residue_management_nodes,
3311
+ target_term_ids=get_residue_removed_or_burnt_terms(),
3312
+ cumulative_threshold=MIN_AREA_THRESHOLD
3815
3313
  )
3816
3314
 
3817
3315
  num_grassland_improvements = [
@@ -3821,18 +3319,18 @@ def _make_carbon_input_args(
3821
3319
  has_organic_fertiliser_or_soil_amendment_used
3822
3320
  ].count(True)
3823
3321
 
3824
- return CarbonInputArgs(
3825
- num_grassland_improvements=num_grassland_improvements,
3826
- has_irrigation=has_irrigation,
3827
- has_residue_removed_or_burnt=has_residue_removed_or_burnt,
3828
- has_low_residue_producing_crops=has_low_residue_producing_crops,
3829
- has_bare_fallow=has_bare_fallow,
3830
- has_n_fixing_crop_or_inorganic_n_fertiliser_used=has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3831
- has_practice_increasing_c_input=has_practice_increasing_c_input,
3832
- has_cover_crop=has_cover_crop,
3833
- has_organic_fertiliser_or_soil_amendment_used=has_organic_fertiliser_or_soil_amendment_used,
3834
- has_animal_manure_used=has_animal_manure_used
3835
- )
3322
+ return {
3323
+ "has_animal_manure_used": has_animal_manure_used,
3324
+ "has_bare_fallow": has_bare_fallow,
3325
+ "has_cover_crop": has_cover_crop,
3326
+ "has_irrigation": has_irrigation,
3327
+ "has_low_residue_producing_crops": has_low_residue_producing_crops,
3328
+ "has_n_fixing_crop_or_inorganic_n_fertiliser_used": has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3329
+ "has_organic_fertiliser_or_soil_amendment_used": has_organic_fertiliser_or_soil_amendment_used,
3330
+ "has_practice_increasing_c_input": has_practice_increasing_c_input,
3331
+ "has_residue_removed_or_burnt": has_residue_removed_or_burnt,
3332
+ "num_grassland_improvements": num_grassland_improvements
3333
+ }
3836
3334
 
3837
3335
 
3838
3336
  GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE = {
@@ -3908,14 +3406,12 @@ def _assign_ipcc_carbon_input_category(
3908
3406
  decision_tree = DECISION_TREE_FROM_IPCC_MANAGEMENT_CATEGORY.get(ipcc_management_category, {})
3909
3407
  default = DEFAULT_CARBON_INPUT_CATEGORY.get(ipcc_management_category, IpccCarbonInputCategory.OTHER)
3910
3408
 
3911
- carbon_input_args = _make_carbon_input_args(management_nodes)
3912
-
3913
3409
  should_run = len(management_nodes) > 0
3914
3410
 
3915
3411
  return next(
3916
3412
  (key for key in decision_tree if decision_tree[key](
3917
- carbon_input_args,
3918
3413
  key=key,
3414
+ **_get_carbon_input_kwargs(management_nodes)
3919
3415
  )),
3920
3416
  default
3921
3417
  ) if should_run else default
@@ -3924,50 +3420,477 @@ def _assign_ipcc_carbon_input_category(
3924
3420
  # --- TIER 1 SOC MODEL ---
3925
3421
 
3926
3422
 
3927
- def _should_run_inventory_year(inner_dict: dict) -> bool:
3423
+ def _run_tier_1(
3424
+ inventory: dict,
3425
+ *,
3426
+ eco_climate_zone: int,
3427
+ soc_ref: float,
3428
+ **_
3429
+ ) -> list[dict]:
3928
3430
  """
3929
- Check if the given inventory year meets the required conditions for further processing.
3431
+ Run the IPCC (2019) Tier 1 methodology for calculating SOC stocks (in kg C ha-1) for each year in the inventory
3432
+ and wrap each of the calculated values in Hestia measurement nodes. To avoid any errors, the `inventory` parameter
3433
+ must be pre-validated by the `should_run` function.
3930
3434
 
3931
- Check if the land use category is not "OTHER" and all required keys are present.
3435
+ See [IPCC (2019) Vol. 4, Ch. 2](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
3436
+
3437
+ The inventory should be in the following shape:
3438
+ ```
3439
+ {
3440
+ year (int): {
3441
+ _InventoryKey.SHOULD_RUN_TIER_1: bool,
3442
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory,
3443
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory,
3444
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory
3445
+ },
3446
+ ...
3447
+ }
3448
+ ```
3932
3449
 
3933
3450
  Parameters
3934
3451
  ----------
3935
- inner_dict : dict
3936
- Dictionary containing information for a specific inventory year.
3452
+ inventory : dict
3453
+ The inventory built by the `_should_run` function.
3454
+ eco_climate_zone : int
3455
+ The eco-climate zone identifier for the site corresponding to a row in the
3456
+ [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
3457
+ lookup table.
3458
+ ipcc_soil_category : IpccSoilCategory
3459
+ The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
3937
3460
 
3938
3461
  Returns
3939
3462
  -------
3940
- bool
3941
- True if the inventory year should be considered, False otherwise.
3463
+ list[dict]
3464
+ A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
3942
3465
  """
3943
- REQUIRED_KEYS = {IpccLandUseCategory, IpccManagementCategory, IpccCarbonInputCategory}
3944
- return (
3945
- inner_dict[IpccLandUseCategory] != IpccLandUseCategory.OTHER
3946
- and all(key in REQUIRED_KEYS for key in inner_dict)
3466
+
3467
+ valid_inventory = {
3468
+ year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_1)
3469
+ }
3470
+
3471
+ timestamps = [year for year in valid_inventory.keys()]
3472
+ ipcc_land_use_categories = [group[_InventoryKey.LU_CATEGORY] for group in valid_inventory.values()]
3473
+ ipcc_management_categories = [group[_InventoryKey.MG_CATEGORY] for group in valid_inventory.values()]
3474
+ ipcc_carbon_input_categories = [group[_InventoryKey.CI_CATEGORY] for group in valid_inventory.values()]
3475
+
3476
+ iterated_timestamps, iterated_soc_equilibriums = _run_soc_equilibriums(
3477
+ timestamps,
3478
+ ipcc_land_use_categories,
3479
+ ipcc_management_categories,
3480
+ ipcc_carbon_input_categories,
3481
+ eco_climate_zone,
3482
+ soc_ref
3947
3483
  )
3948
3484
 
3485
+ soc_stocks = _calc_tier_1_soc_stocks(iterated_timestamps, iterated_soc_equilibriums)
3949
3486
 
3950
- def _should_run_tier_1(site: dict) -> tuple:
3487
+ return [
3488
+ _measurement(
3489
+ year,
3490
+ soc_stock,
3491
+ MeasurementMethodClassification.TIER_1_MODEL.value
3492
+ ) for year, soc_stock in zip(
3493
+ iterated_timestamps,
3494
+ soc_stocks
3495
+ )
3496
+ ]
3497
+
3498
+
3499
+ # --- SHOULD RUN ---
3500
+
3501
+
3502
+ def _should_run(site: dict) -> tuple[bool, dict]:
3503
+ """
3504
+ Extract data from site & related cycles, pre-process data and determine whether there is sufficient data to run the
3505
+ tier 1 and/or tier 2 model.
3506
+
3507
+ The inventory dict should be in the following shape:
3508
+ ```
3509
+ {
3510
+ year (int): {
3511
+ _InventoryKey.SHOULD_RUN_TIER_2: bool,
3512
+ _InventoryKey.TEMP_MONTHLY: list[float],
3513
+ _InventoryKey.PRECIP_MONTHLY: list[float],
3514
+ _InventoryKey.PET_MONTHLY: list[float],
3515
+ _InventoryKey.IRRIGATED_MONTHLY: list[bool]
3516
+ _InventoryKey.CARBON_INPUT: float,
3517
+ _InventoryKey.N_CONTENT: float,
3518
+ _InventoryKey.TILLAGE_CATEGORY: IpccManagementCategory,
3519
+ _InventoryKey.SAND_CONTENT: float,
3520
+ _InventoryKey.SHOULD_RUN_TIER_1: bool,
3521
+ _InventoryKey.LU_CATEGORY: IpccLandUseCategory,
3522
+ _InventoryKey.MG_CATEGORY: IpccManagementCategory,
3523
+ _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory
3524
+ },
3525
+ ...
3526
+ }
3527
+ ```
3528
+
3529
+ The kwargs dict should be in the following shape:
3530
+ ```
3531
+ {
3532
+ "run_with_irrigation": bool,
3533
+ "eco_climate_zone": int,
3534
+ "ipcc_soil_category": IpccSoilCategory,
3535
+ "soc_ref": float
3536
+ }
3537
+ ```
3951
3538
  """
3952
- Determines whether Tier 1 of the IPCC SOC model should run for a given site.
3539
+ site_type = site.get("siteType", "")
3540
+ management_nodes = site.get("management", [])
3541
+ measurement_nodes = site.get("measurements", [])
3542
+ cycles = related_cycles(site.get("@id"))
3543
+
3544
+ has_management = len(management_nodes) > 0
3545
+ has_measurements = len(measurement_nodes) > 0
3546
+ has_related_cycles = len(cycles) > 0
3547
+ has_functional_unit_1_ha = all(cycle.get("functionalUnit") in VALID_FUNCTIONAL_UNITS_TIER_2 for cycle in cycles)
3548
+
3549
+ should_build_inventory_tier_1 = all([
3550
+ site_type in VALID_SITE_TYPES_TIER_1,
3551
+ has_management,
3552
+ has_measurements
3553
+ ])
3554
+
3555
+ should_build_inventory_tier_2 = all([
3556
+ site_type in VALID_SITE_TYPES_TIER_2,
3557
+ has_related_cycles,
3558
+ check_cycle_site_ids_identical(cycles),
3559
+ has_functional_unit_1_ha
3560
+ ])
3561
+
3562
+ inventory_tier_1, kwargs_tier_1 = (
3563
+ _build_inventory_tier_1(site_type, management_nodes, measurement_nodes)
3564
+ if should_build_inventory_tier_1 else ({}, {})
3565
+ )
3566
+
3567
+ inventory_tier_2, kwargs_tier_2 = (
3568
+ _build_inventory_tier_2(cycles, measurement_nodes)
3569
+ if should_build_inventory_tier_2 else ({}, {})
3570
+ )
3571
+
3572
+ inventory = dict(sorted(merge(inventory_tier_1, inventory_tier_2).items()))
3573
+ kwargs = kwargs_tier_1 | kwargs_tier_2
3574
+
3575
+ should_run_tier_1 = _should_run_tier_1(inventory, **kwargs) if should_build_inventory_tier_1 else False
3576
+ should_run_tier_2 = _should_run_tier_2(inventory, **kwargs) if should_build_inventory_tier_2 else False
3577
+
3578
+ logRequirements(
3579
+ site, model=MODEL, term=TERM_ID,
3580
+ should_build_inventory_tier_1=should_build_inventory_tier_1,
3581
+ should_build_inventory_tier_2=should_build_inventory_tier_2,
3582
+ should_run_tier_1=should_run_tier_1,
3583
+ should_run_tier_2=should_run_tier_2,
3584
+ site_type=site_type,
3585
+ has_management=has_management,
3586
+ has_measurements=has_measurements,
3587
+ has_related_cycles=has_related_cycles,
3588
+ is_unit_hectare=has_functional_unit_1_ha,
3589
+ **kwargs,
3590
+ inventory=_log_inventory(inventory)
3591
+ )
3592
+
3593
+ should_run = should_run_tier_1 or should_run_tier_2
3594
+ logShouldRun(site, MODEL, TERM_ID, should_run)
3595
+
3596
+ return should_run_tier_1, should_run_tier_2, inventory, kwargs
3597
+
3598
+
3599
+ def _should_run_tier_1(
3600
+ inventory: dict,
3601
+ *,
3602
+ eco_climate_zone: int = None,
3603
+ soc_ref: float = None,
3604
+ **_
3605
+ ) -> bool:
3606
+ """
3607
+ Determines whether there is sufficient data in the inventory and keyword args to run the tier 1 model.
3608
+ """
3609
+ return all([
3610
+ eco_climate_zone and eco_climate_zone > 0,
3611
+ soc_ref and soc_ref > 0,
3612
+ any(year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_1))
3613
+ ])
3614
+
3615
+
3616
+ def _should_run_tier_2(
3617
+ inventory: dict,
3618
+ **_
3619
+ ) -> bool:
3620
+ """
3621
+ Determines whether there is sufficient data in the inventory and keyword args to run the tier 2 model.
3622
+ """
3623
+ valid_years = [year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_2)]
3624
+ return all([
3625
+ len(valid_years) >= MIN_RUN_IN_PERIOD,
3626
+ check_consecutive(valid_years),
3627
+ any(inventory.get(year).get(_InventoryKey.SAND_CONTENT) for year in valid_years)
3628
+ ])
3629
+
3630
+
3631
+ # --- LOGGING ---
3632
+
3633
+
3634
+ def _log_inventory(inventory: dict) -> str:
3635
+ """
3636
+ Format the inventory data as a table for logging.
3637
+ """
3638
+ log_table = log_as_table(
3639
+ {
3640
+ "year": year,
3641
+ "should-run-tier-1": group.get(_InventoryKey.SHOULD_RUN_TIER_1, False),
3642
+ "should-run-tier-2": group.get(_InventoryKey.SHOULD_RUN_TIER_2, False),
3643
+ "ipcc-land-use-category": (
3644
+ group.get(_InventoryKey.LU_CATEGORY).value if group.get(_InventoryKey.LU_CATEGORY) else None
3645
+ ),
3646
+ "ipcc-management-category": (
3647
+ group.get(_InventoryKey.MG_CATEGORY).value if group.get(_InventoryKey.MG_CATEGORY) else None
3648
+ ),
3649
+ "ipcc-carbon-input-category": (
3650
+ group.get(_InventoryKey.CI_CATEGORY).value if group.get(_InventoryKey.CI_CATEGORY) else None
3651
+ ),
3652
+ "temperature-monthly": (
3653
+ " ".join(f"{val:.1f}" for val in group.get(_InventoryKey.TEMP_MONTHLY))
3654
+ if group.get(_InventoryKey.TEMP_MONTHLY) else None
3655
+ ),
3656
+ "precipitation-monthly": (
3657
+ " ".join(f"{val:.1f}" for val in group.get(_InventoryKey.PRECIP_MONTHLY))
3658
+ if group.get(_InventoryKey.PRECIP_MONTHLY) else None
3659
+ ),
3660
+ "pet-monthly": (
3661
+ " ".join(f"{val:.1f}" for val in group.get(_InventoryKey.PET_MONTHLY))
3662
+ if group.get(_InventoryKey.PET_MONTHLY) else None
3663
+ ),
3664
+ "irrigated-monthly": (
3665
+ " ".join(str(val) for val in group.get(_InventoryKey.IRRIGATED_MONTHLY))
3666
+ if group.get(_InventoryKey.PET_MONTHLY) else None
3667
+ ),
3668
+ "sand-content": group.get(_InventoryKey.SAND_CONTENT, None),
3669
+ "carbon-input": group.get(_InventoryKey.CARBON_INPUT, None),
3670
+ "n-content": group.get(_InventoryKey.N_CONTENT, None),
3671
+ "lignin-content": group.get(_InventoryKey.LIGNIN_CONTENT, None),
3672
+ "ipcc-tillage-category": (
3673
+ group.get(_InventoryKey.TILLAGE_CATEGORY).value if group.get(_InventoryKey.TILLAGE_CATEGORY) else None
3674
+ ),
3675
+ "is-paddy-rice": group.get(_InventoryKey.IS_PADDY_RICE, None),
3676
+ } for year, group in inventory.items()
3677
+ )
3678
+
3679
+ return log_table or None
3680
+
3681
+
3682
+ # --- TIER 2 BUILD INVENTORY ---
3683
+
3684
+
3685
+ def _build_inventory_tier_2(
3686
+ cycles: list[dict], measurement_nodes: list[dict]
3687
+ ) -> tuple[dict, dict]:
3688
+ """
3689
+ Builds an annual inventory of data and a dictionary of keyword arguments for the tier 2 model.
3690
+
3691
+ TODO: implement long-term average climate data and annual climate data as back ups for monthly data
3692
+ """
3693
+ grouped_cycles = group_nodes_by_year(cycles)
3694
+ grouped_measurements = group_nodes_by_year(measurement_nodes, mode=GroupNodesByYearMode.DATES)
3695
+
3696
+ grouped_climate_data = _get_grouped_climate_measurements(grouped_measurements)
3697
+ grouped_irrigated_monthly = _get_grouped_irrigated_monthly(grouped_cycles)
3698
+ grouped_sand_content_measurements = _get_grouped_sand_content_measurements(grouped_measurements)
3699
+ grouped_carbon_input_data = _get_grouped_carbon_input_data(grouped_cycles)
3700
+ grouped_tillage_categories = _get_grouped_tillage_categories(grouped_cycles)
3701
+ grouped_is_paddy_rice = _get_grouped_is_paddy_rice(grouped_cycles)
3702
+
3703
+ grouped_data = merge(
3704
+ grouped_climate_data,
3705
+ grouped_irrigated_monthly,
3706
+ grouped_sand_content_measurements,
3707
+ grouped_carbon_input_data,
3708
+ grouped_tillage_categories,
3709
+ grouped_is_paddy_rice
3710
+ )
3711
+
3712
+ grouped_should_run = {
3713
+ year: {_InventoryKey.SHOULD_RUN_TIER_2: _should_run_inventory_year_tier_2(group)}
3714
+ for year, group in grouped_data.items()
3715
+ }
3716
+
3717
+ inventory = merge(grouped_data, grouped_should_run)
3718
+ kwargs = {
3719
+ "run_with_irrigation": True
3720
+ }
3721
+
3722
+ return inventory, kwargs
3723
+
3724
+
3725
+ def _should_run_inventory_year_tier_2(group: dict) -> bool:
3726
+ """
3727
+ Determines whether there is sufficient data in an inventory year to run the tier 2 model.
3728
+
3729
+ 1. Check that the cycle is not for paddy rice.
3730
+ 2. Check if monthly data has a value for each calendar month.
3731
+ 3. Check if all required keys are present.
3953
3732
 
3954
3733
  Parameters
3955
3734
  ----------
3956
- site : dict
3957
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
3735
+ group : dict
3736
+ Dictionary containing information for a specific inventory year.
3958
3737
 
3959
3738
  Returns
3960
3739
  -------
3961
- tuple
3962
- A tuple containing information to determine if Tier 1 should run, including timestamps,
3963
- IPCC land use categories, management categories, carbon input categories, eco-climate zone,
3964
- SOC reference, and initial SOC stock.
3965
- """
3740
+ bool
3741
+ True if the inventory year is valid, False otherwise.
3742
+ """
3743
+ monthly_data_complete = _check_12_months(
3744
+ group,
3745
+ {
3746
+ _InventoryKey.TEMP_MONTHLY,
3747
+ _InventoryKey.PRECIP_MONTHLY,
3748
+ _InventoryKey.PET_MONTHLY,
3749
+ _InventoryKey.IRRIGATED_MONTHLY
3750
+ }
3751
+ )
3752
+
3753
+ return all([
3754
+ not group.get(_InventoryKey.IS_PADDY_RICE),
3755
+ monthly_data_complete,
3756
+ all(key in group.keys() for key in REQUIRED_KEYS_TIER_2),
3757
+ ])
3966
3758
 
3967
- site_type = site.get("siteType", "")
3968
- management_nodes = site.get("management", [])
3969
- measurement_nodes = site.get("measurements", [])
3970
3759
 
3760
+ def _get_grouped_climate_measurements(grouped_measurements: dict) -> dict:
3761
+ return {
3762
+ year: {
3763
+ _InventoryKey.TEMP_MONTHLY: find_term_match(measurements, TEMPERATURE_MONTHLY_TERM_ID, {}).get("value", []),
3764
+ _InventoryKey.PRECIP_MONTHLY: (
3765
+ find_term_match(measurements, PRECIPITATION_MONTHLY_TERM_ID, {}).get("value", [])
3766
+ ),
3767
+ _InventoryKey.PET_MONTHLY: find_term_match(measurements, PET_MONTHLY_TERM_ID, {}).get("value", [])
3768
+ } for year, measurements in grouped_measurements.items()
3769
+ }
3770
+
3771
+
3772
+ def _get_grouped_irrigated_monthly(grouped_cycles: dict) -> dict:
3773
+ return {
3774
+ year: {
3775
+ _InventoryKey.IRRIGATED_MONTHLY: _get_irrigated_monthly(year, cycles)
3776
+ } for year, cycles in grouped_cycles.items()
3777
+ }
3778
+
3779
+
3780
+ def _get_irrigated_monthly(year: int, cycles: list[dict]) -> list[bool]:
3781
+ # Get practice nodes and add "startDate" and "endDate" from cycle if missing.
3782
+ irrigation_nodes = non_empty_list(flatten([
3783
+ [
3784
+ {
3785
+ "startDate": cycle.get("startDate"),
3786
+ "endDate": cycle.get("endDate"),
3787
+ **node
3788
+ } for node in cycle.get("practices", [])
3789
+ ] for cycle in cycles
3790
+ ]))
3791
+
3792
+ grouped_nodes = group_nodes_by_year_and_month(irrigation_nodes)
3793
+
3794
+ # For each month (1 - 12) check if irrigation is present.
3795
+ return [
3796
+ cumulative_nodes_term_match(
3797
+ grouped_nodes.get(year, {}).get(month, []),
3798
+ target_term_ids=get_irrigated_terms(),
3799
+ cumulative_threshold=MIN_AREA_THRESHOLD
3800
+ ) for month in range(1, 13)
3801
+ ]
3802
+
3803
+
3804
+ def _get_grouped_sand_content_measurements(grouped_measurements: dict) -> dict:
3805
+ grouped_sand_content_measurements = {
3806
+ year: find_term_match(
3807
+ [m for m in measurements if m.get("depthUpper") == DEPTH_UPPER and m.get("depthLower") == DEPTH_LOWER],
3808
+ SAND_CONTENT_TERM_ID,
3809
+ {}
3810
+ ) for year, measurements in grouped_measurements.items()
3811
+ }
3812
+
3813
+ return {
3814
+ year: {_InventoryKey.SAND_CONTENT: get_node_value(measurement)}
3815
+ for year, measurement in grouped_sand_content_measurements.items() if measurement
3816
+ }
3817
+
3818
+
3819
+ def _get_grouped_carbon_input_data(grouped_cycles: dict) -> dict:
3820
+ grouped_carbon_sources = {
3821
+ year: _get_carbon_sources_from_cycles(cycle)
3822
+ for year, cycle in grouped_cycles.items()
3823
+ }
3824
+
3825
+ return {
3826
+ year: {
3827
+ _InventoryKey.CARBON_INPUT: _calc_total_organic_carbon_input(carbon_sources),
3828
+ _InventoryKey.N_CONTENT: _calc_average_nitrogen_content_of_organic_carbon_sources(carbon_sources),
3829
+ _InventoryKey.LIGNIN_CONTENT: _calc_average_lignin_content_of_organic_carbon_sources(carbon_sources)
3830
+ } for year, carbon_sources in grouped_carbon_sources.items()
3831
+ }
3832
+
3833
+
3834
+ def _get_grouped_tillage_categories(grouped_cycles):
3835
+ return {
3836
+ year: {
3837
+ _InventoryKey.TILLAGE_CATEGORY: _assign_tier_2_ipcc_tillage_management_category(cycles)
3838
+ } for year, cycles in grouped_cycles.items()
3839
+ }
3840
+
3841
+
3842
+ def _get_grouped_is_paddy_rice(grouped_cycles: dict) -> dict:
3843
+ return {
3844
+ year: {
3845
+ _InventoryKey.IS_PADDY_RICE: _check_is_paddy_rice(cycles)
3846
+ } for year, cycles in grouped_cycles.items()
3847
+ }
3848
+
3849
+
3850
+ def _check_is_paddy_rice(cycles: list[dict]) -> bool:
3851
+ LOOKUP = LOOKUPS["crop"]
3852
+ TARGET_LOOKUP_VALUES = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(
3853
+ IpccLandUseCategory.PADDY_RICE_CULTIVATION, None
3854
+ )
3855
+
3856
+ has_paddy_rice_products = any(cumulative_nodes_lookup_match(
3857
+ filter_list_term_type(
3858
+ cycle.get("products", []) + cycle.get("practices", []),
3859
+ [TermTermType.CROP, TermTermType.FORAGE, TermTermType.LANDCOVER]
3860
+ ),
3861
+ lookup=LOOKUP,
3862
+ target_lookup_values=TARGET_LOOKUP_VALUES,
3863
+ cumulative_threshold=MIN_YIELD_THRESHOLD,
3864
+ default_node_value=MIN_YIELD_THRESHOLD
3865
+ ) for cycle in cycles)
3866
+
3867
+ has_upland_rice_products = any(cumulative_nodes_term_match(
3868
+ filter_list_term_type(
3869
+ cycle.get("products", []) + cycle.get("practices", []),
3870
+ [TermTermType.CROP, TermTermType.FORAGE, TermTermType.LANDCOVER]
3871
+ ),
3872
+ target_term_ids=get_upland_rice_crop_terms() + get_upland_rice_land_cover_terms(),
3873
+ cumulative_threshold=MIN_YIELD_THRESHOLD,
3874
+ default_node_value=MIN_YIELD_THRESHOLD
3875
+ ) for cycle in cycles)
3876
+
3877
+ has_irrigation = any(
3878
+ _has_irrigation(filter_list_term_type(cycle.get("practices", []), [TermTermType.WATERREGIME]))
3879
+ for cycle in cycles
3880
+ )
3881
+
3882
+ return has_paddy_rice_products or (has_upland_rice_products and has_irrigation)
3883
+
3884
+
3885
+ # --- TIER 1 BUILD INVENTORY ---
3886
+
3887
+
3888
+ def _build_inventory_tier_1(
3889
+ site_type: str, management_nodes: list[dict], measurement_nodes: list[dict]
3890
+ ) -> tuple[dict, dict]:
3891
+ """
3892
+ Builds an annual inventory of data and a dictionary of keyword arguments for the tier 2 model.
3893
+ """
3971
3894
  eco_climate_zone = _get_eco_climate_zone(measurement_nodes)
3972
3895
  ipcc_soil_category = _assign_ipcc_soil_category(measurement_nodes)
3973
3896
  soc_ref = _retrieve_soc_ref(eco_climate_zone, ipcc_soil_category)
@@ -3976,7 +3899,7 @@ def _should_run_tier_1(site: dict) -> tuple:
3976
3899
 
3977
3900
  grouped_land_use_categories = {
3978
3901
  year: {
3979
- IpccLandUseCategory: _assign_ipcc_land_use_category(
3902
+ _InventoryKey.LU_CATEGORY: _assign_ipcc_land_use_category(
3980
3903
  site_type,
3981
3904
  nodes,
3982
3905
  ipcc_soil_category
@@ -3986,171 +3909,72 @@ def _should_run_tier_1(site: dict) -> tuple:
3986
3909
 
3987
3910
  grouped_management_categories = {
3988
3911
  year: {
3989
- IpccManagementCategory: _assign_ipcc_management_category(
3912
+ _InventoryKey.MG_CATEGORY: _assign_ipcc_management_category(
3990
3913
  nodes,
3991
- grouped_land_use_categories[year][IpccLandUseCategory]
3914
+ grouped_land_use_categories[year][_InventoryKey.LU_CATEGORY]
3992
3915
  )
3993
3916
  } for year, nodes in grouped_management.items()
3994
3917
  }
3995
3918
 
3996
3919
  grouped_carbon_input_categories = {
3997
3920
  year: {
3998
- IpccCarbonInputCategory: _assign_ipcc_carbon_input_category(
3921
+ _InventoryKey.CI_CATEGORY: _assign_ipcc_carbon_input_category(
3999
3922
  nodes,
4000
- grouped_management_categories[year][IpccManagementCategory]
3923
+ grouped_management_categories[year][_InventoryKey.MG_CATEGORY]
4001
3924
  )
4002
3925
  } for year, nodes in grouped_management.items()
4003
3926
  }
4004
3927
 
4005
- grouped_data = reduce(merge, [
3928
+ grouped_data = merge(
4006
3929
  grouped_land_use_categories,
4007
3930
  grouped_management_categories,
4008
3931
  grouped_carbon_input_categories
4009
- ])
4010
-
4011
- complete_data = {
4012
- year: inner_dict for year, inner_dict in grouped_data.items()
4013
- if _should_run_inventory_year(inner_dict)
4014
- }
4015
-
4016
- timestamps = list(complete_data)
4017
- start_year = timestamps[0] if timestamps else None
4018
- end_year = timestamps[-1] if timestamps else None
4019
-
4020
- soc_measurement_value, soc_measurement_date = most_relevant_measurement_value_by_depth_and_date(
4021
- measurement_nodes,
4022
- TERM_ID,
4023
- f"{end_year}-12-31", # Last year in inventory
4024
- DEPTH_UPPER,
4025
- DEPTH_LOWER,
4026
- depth_strict=True
4027
- ) if end_year else (None, None)
4028
-
4029
- soc_measurement_datetime = safe_parse_date(soc_measurement_date)
4030
- soc_measurement_year = (
4031
- soc_measurement_datetime.year if soc_measurement_datetime else None
4032
3932
  )
4033
3933
 
4034
- ipcc_land_use_categories = [complete_data[year][IpccLandUseCategory] for year in timestamps]
4035
- ipcc_management_categories = [complete_data[year][IpccManagementCategory] for year in timestamps]
4036
- ipcc_carbon_input_categories = [complete_data[year][IpccCarbonInputCategory] for year in timestamps]
4037
-
4038
- should_run = all([
4039
- soc_ref > 0,
4040
- isinstance(eco_climate_zone, int) and eco_climate_zone > 0,
4041
- len(timestamps) > 0,
4042
- (
4043
- start_year <= soc_measurement_year < end_year
4044
- ) if soc_measurement_value and soc_measurement_year else True
4045
- ])
3934
+ grouped_should_run = {
3935
+ year: {_InventoryKey.SHOULD_RUN_TIER_1: _should_run_inventory_year_tier_1(group)}
3936
+ for year, group in grouped_data.items()
3937
+ }
4046
3938
 
4047
- logShouldRun(
4048
- site, MODEL, TERM_ID, should_run,
4049
- timestamps=log_as_table(set(timestamps)),
4050
- ipcc_land_use_categories=log_as_table(set(ipcc_land_use_categories)),
4051
- ipcc_management_categories=log_as_table(set(ipcc_management_categories)),
4052
- ipcc_carbon_input_categories=log_as_table(set(ipcc_carbon_input_categories)),
4053
- eco_climate_zone=eco_climate_zone,
4054
- soc_ref=soc_ref,
4055
- run_with_soc_measurement=(soc_measurement_value and soc_measurement_year),
4056
- soc_measurement_value=soc_measurement_value,
4057
- soc_measurement_year=soc_measurement_year
4058
- )
3939
+ inventory = merge(grouped_data, grouped_should_run)
3940
+ kwargs = {
3941
+ "eco_climate_zone": eco_climate_zone,
3942
+ "ipcc_soil_category": ipcc_soil_category,
3943
+ "soc_ref": soc_ref,
3944
+ }
4059
3945
 
4060
- return (
4061
- should_run,
4062
- timestamps,
4063
- ipcc_land_use_categories,
4064
- ipcc_management_categories,
4065
- ipcc_carbon_input_categories,
4066
- eco_climate_zone,
4067
- soc_ref,
4068
- soc_measurement_value,
4069
- soc_measurement_year
4070
- )
3946
+ return inventory, kwargs
4071
3947
 
4072
3948
 
4073
- def _run_tier_1(
4074
- timestamps: list[int],
4075
- ipcc_land_use_categories: list[IpccLandUseCategory],
4076
- ipcc_management_categories: list[IpccManagementCategory],
4077
- ipcc_carbon_input_categories: list[IpccCarbonInputCategory],
4078
- eco_climate_zone: int,
4079
- soc_ref: float,
4080
- soc_measurement_value: Optional[float] = None,
4081
- soc_measurement_year: Optional[int] = None
4082
- ) -> list[dict]:
3949
+ def _should_run_inventory_year_tier_1(group: dict) -> bool:
4083
3950
  """
4084
- Run the IPCC (2019) Tier 1 methodology for calculating SOC stocks (in kg C ha-1) for each year in the inventory
4085
- and wrap each of the calculated values in Hestia measurement nodes.
3951
+ Determines whether there is sufficient data in an inventory year to run the tier 1 model.
4086
3952
 
4087
- See [IPCC (2019) Vol. 4, Ch. 2](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
3953
+ 1. Check if the land use category is not "OTHER"
3954
+ 2. Check if all required keys are present.
4088
3955
 
4089
3956
  Parameters
4090
3957
  ----------
4091
- timestamps : list[int]
4092
- A list of timestamps for each year in the inventory.
4093
- ipcc_land_use_categories : list[IpccLandUseCategory]
4094
- A list of IPCC land use categories for each year in the inventory.
4095
- ipcc_management_categories : list[IpccManagementCategory]
4096
- A list of IPCC management categories for each year in the inventory.
4097
- ipcc_carbon_input_categories : list[IpccCarbonInputCategory]
4098
- A list of IPCC carbon input categories for each year in the inventory.
4099
- eco_climate_zone : int
4100
- The eco-climate zone identifier for the site corresponding to a row in the
4101
- [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
4102
- lookup table.
4103
- ipcc_soil_category : IpccSoilCategory
4104
- The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
4105
- soc_measurement_value : float | None, optional
4106
- Measured SOC stock, if provided.
4107
- soc_measurement_year : int | None, optional
4108
- The year the SOC measurement took place, if provided.
3958
+ group : dict
3959
+ Dictionary containing information for a specific inventory year.
4109
3960
 
4110
3961
  Returns
4111
3962
  -------
4112
- tuple
3963
+ bool
3964
+ True if the inventory year is valid, False otherwise.
4113
3965
  """
4114
-
4115
- iterated_timestamps, iterated_soc_equilibriums = _run_soc_equilibriums(
4116
- timestamps,
4117
- ipcc_land_use_categories,
4118
- ipcc_management_categories,
4119
- ipcc_carbon_input_categories,
4120
- eco_climate_zone,
4121
- soc_ref
4122
- )
4123
-
4124
- soc_stocks = _calc_tier_1_soc_stocks(iterated_timestamps, iterated_soc_equilibriums)
4125
-
4126
- soc_measurement_index = _find_closest_value_index(iterated_timestamps, soc_measurement_year)
4127
-
4128
- scaled_soc_stocks = _scale_soc_stocks(
4129
- soc_stocks, soc_measurement_value, soc_measurement_index
4130
- )
4131
-
4132
- slice_index = soc_measurement_index or 0
4133
- result_timestamps = iterated_timestamps[slice_index:]
4134
- result_soc_stocks = scaled_soc_stocks[slice_index:]
4135
-
4136
- return [
4137
- _measurement(
4138
- year,
4139
- soc_stock,
4140
- MeasurementMethodClassification.TIER_1_MODEL.value
4141
- ) for year, soc_stock in zip(
4142
- result_timestamps,
4143
- result_soc_stocks
4144
- )
4145
- ]
3966
+ return all([
3967
+ group.get(_InventoryKey.LU_CATEGORY) != IpccLandUseCategory.OTHER,
3968
+ all(key in group.keys() for key in REQUIRED_KEYS_TIER_1),
3969
+ ])
4146
3970
 
4147
3971
 
4148
- # --- SHARED TIER 1 & TIER 2 RUN FUNCTION ---
3972
+ # --- RUN ---
4149
3973
 
4150
3974
 
4151
3975
  def run(site: dict) -> list[dict]:
4152
3976
  """
4153
- Check which Tier of IPCC SOC model to run, run it and return the formatted output.
3977
+ Check which Tiers of IPCC SOC model to run, run it and return the formatted output.
4154
3978
 
4155
3979
  Parameters
4156
3980
  ----------
@@ -4162,13 +3986,9 @@ def run(site: dict) -> list[dict]:
4162
3986
  list[dict]
4163
3987
  A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
4164
3988
  """
4165
- should_run_tier_2, *tier_2_args = _should_run_tier_2(site)
4166
- should_run_tier_1, *tier_1_args = (
4167
- _should_run_tier_1(site) if not should_run_tier_2
4168
- else (False, None)
4169
- )
3989
+ should_run_tier_1, should_run_tier_2, inventory, kwargs = _should_run(site)
4170
3990
  return (
4171
- _run_tier_2(*tier_2_args) if should_run_tier_2
4172
- else _run_tier_1(*tier_1_args) if should_run_tier_1
3991
+ _run_tier_2(inventory, **kwargs) if should_run_tier_2
3992
+ else _run_tier_1(inventory, **kwargs) if should_run_tier_1
4173
3993
  else []
4174
3994
  )