hestia-earth-models 0.58.0__py3-none-any.whl → 0.59.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hestia-earth-models might be problematic. Click here for more details.

Files changed (34) hide show
  1. hestia_earth/models/cycle/{irrigated.py → irrigatedTypeUnspecified.py} +4 -4
  2. hestia_earth/models/cycle/residueIncorporated.py +1 -1
  3. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +2 -2
  4. hestia_earth/models/geospatialDatabase/clayContent.py +17 -4
  5. hestia_earth/models/geospatialDatabase/sandContent.py +17 -4
  6. hestia_earth/models/impact_assessment/irrigated.py +0 -3
  7. hestia_earth/models/ipcc2019/co2ToAirSoilCarbonStockChangeManagementChange.py +10 -9
  8. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +4 -51
  9. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserDirect.py +104 -0
  10. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserDirect.py +105 -0
  11. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +1059 -1220
  12. hestia_earth/models/ipcc2019/utils.py +82 -1
  13. hestia_earth/models/mocking/search-results.json +161 -87
  14. hestia_earth/models/site/management.py +12 -9
  15. hestia_earth/models/site/organicCarbonPerHa.py +251 -89
  16. hestia_earth/models/utils/blank_node.py +157 -34
  17. hestia_earth/models/utils/cycle.py +6 -3
  18. hestia_earth/models/utils/measurement.py +1 -1
  19. hestia_earth/models/utils/term.py +46 -1
  20. hestia_earth/models/version.py +1 -1
  21. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/METADATA +4 -8
  22. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/RECORD +34 -30
  23. tests/models/cycle/{test_irrigated.py → test_irrigatedTypeUnspecified.py} +1 -1
  24. tests/models/geospatialDatabase/test_clayContent.py +9 -3
  25. tests/models/geospatialDatabase/test_sandContent.py +9 -3
  26. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserDirect.py +74 -0
  27. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserDirect.py +74 -0
  28. tests/models/ipcc2019/test_organicCarbonPerHa.py +303 -1044
  29. tests/models/site/test_organicCarbonPerHa.py +51 -5
  30. tests/models/utils/test_blank_node.py +102 -42
  31. tests/models/utils/test_term.py +17 -3
  32. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/LICENSE +0 -0
  33. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/WHEEL +0 -0
  34. {hestia_earth_models-0.58.0.dist-info → hestia_earth_models-0.59.0.dist-info}/top_level.txt +0 -0
@@ -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,8 +47,10 @@ 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
 
59
56
  from .utils import check_consecutive
@@ -116,6 +113,7 @@ REQUIREMENTS = {
116
113
  }
117
114
  }
118
115
  LOOKUPS = {
116
+ "crop": "IPCC_LAND_USE_CATEGORY",
119
117
  "ecoClimateZone": [
120
118
  "IPCC_2019_SOC_REF_KG_C_HECTARE_SAN",
121
119
  "IPCC_2019_SOC_REF_KG_C_HECTARE_WET",
@@ -169,6 +167,7 @@ TERM_ID = 'organicCarbonPerHa'
169
167
 
170
168
  MIN_AREA_THRESHOLD = 30 # 30% as per IPCC guidelines
171
169
  SUPER_MAJORITY_AREA_THRESHOLD = 100 - MIN_AREA_THRESHOLD
170
+ MIN_YIELD_THRESHOLD = 1
172
171
  DEPTH_UPPER = 0
173
172
  DEPTH_LOWER = 30
174
173
 
@@ -222,6 +221,14 @@ DEFAULT_PARAMS = {
222
221
  "default_lignin_content": 0.073
223
222
  }
224
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
+
225
232
  # --- TIER 1 CONSTANTS ---
226
233
 
227
234
  CLAY_CONTENT_TERM_ID = "clayContent"
@@ -232,8 +239,6 @@ ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
232
239
  INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
233
240
  ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserOrSoilCarbonIncreasingAmendmentUsed"
234
241
 
235
- DEFAULT_NODE_VALUE = 100
236
-
237
242
  CLAY_CONTENT_MAX = 8
238
243
  SAND_CONTENT_MIN = 70
239
244
 
@@ -243,6 +248,13 @@ The number of years required for soil organic carbon to reach equilibrium after
243
248
  a change in land use, management regime or carbon input regime.
244
249
  """
245
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
+
246
258
  # --- SHARED TIER 1 & TIER 2 FORMAT MEASUREMENT OUTPUT ---
247
259
 
248
260
 
@@ -275,112 +287,115 @@ def _measurement(year: int, value: float, method_classification: str) -> dict:
275
287
 
276
288
  # --- SHARED TIER 1 & TIER 2 ENUMS ---
277
289
 
278
-
279
- IpccManagementCategory = Enum("IpccManagementCategory", [
280
- "SEVERELY_DEGRADED",
281
- "IMPROVED_GRASSLAND",
282
- "HIGH_INTENSITY_GRAZING",
283
- "NOMINALLY_MANAGED",
284
- "FULL_TILLAGE",
285
- "REDUCED_TILLAGE",
286
- "NO_TILLAGE",
287
- "OTHER"
288
- ])
289
- """
290
- Enum representing IPCC Management Categories for grasslands and annual
291
- croplands.
292
-
293
- See [IPCC (2019) Vol. 4, Ch. 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
294
- """
295
-
296
-
297
- # --- TIER 2 ENUMS ---
298
-
299
-
300
- _InnerKey = Enum(
301
- "_InnerKey",
302
- [
303
- "TEMPERATURES",
304
- "PRECIPITATIONS",
305
- "PETS",
306
- "IS_IRRIGATEDS",
307
- "CARBON_SOURCES",
308
- "TILLAGE_CATEGORY"
309
- ],
310
- )
311
-
312
-
313
- INNER_KEYS_RUN_WITH_IRRIGATION = [
314
- _InnerKey.TEMPERATURES,
315
- _InnerKey.PRECIPITATIONS,
316
- _InnerKey.PETS,
317
- _InnerKey.IS_IRRIGATEDS,
318
- _InnerKey.CARBON_SOURCES,
319
- _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
320
334
  ]
321
335
 
322
- INNER_KEYS_RUN_WITHOUT_IRRIGATION = [
323
- _InnerKey.TEMPERATURES,
324
- _InnerKey.PRECIPITATIONS,
325
- _InnerKey.PETS,
326
- _InnerKey.CARBON_SOURCES,
327
- _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
328
346
  ]
329
347
 
330
348
 
331
349
  # --- TIER 1 ENUMS ---
332
350
 
333
351
 
334
- IpccSoilCategory = Enum("IpccSoilCategory", [
335
- "ORGANIC_SOILS",
336
- "SANDY_SOILS",
337
- "WETLAND_SOILS",
338
- "VOLCANIC_SOILS",
339
- "SPODIC_SOILS",
340
- "HIGH_ACTIVITY_CLAY_SOILS",
341
- "LOW_ACTIVITY_CLAY_SOILS",
342
- ])
343
- """
344
- Enum representing IPCC Soil Categories.
352
+ class IpccSoilCategory(Enum):
353
+ """
354
+ Enum representing IPCC Soil Categories.
345
355
 
346
- See [IPCC (2019) Vol 4, Ch. 2 and 3](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
347
- """
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"
348
366
 
349
367
 
350
- IpccLandUseCategory = Enum("IpccLandUseCategory", [
351
- "GRASSLAND",
352
- "PERENNIAL_CROPS",
353
- "PADDY_RICE_CULTIVATION",
354
- "ANNUAL_CROPS_WET",
355
- "ANNUAL_CROPS",
356
- "SET_ASIDE",
357
- "FOREST",
358
- "NATIVE",
359
- "OTHER"
360
- ])
361
- """
362
- Enum representing IPCC Land Use Categories.
368
+ class IpccLandUseCategory(Enum):
369
+ """
370
+ Enum representing IPCC Land Use Categories.
363
371
 
364
- See [IPCC (2019) Vol 4](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
365
- """
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"
366
383
 
367
384
 
368
- IpccCarbonInputCategory = Enum("IpccCarbonInputCategory", [
369
- "GRASSLAND_HIGH",
370
- "GRASSLAND_MEDIUM",
371
- "CROPLAND_HIGH_WITH_MANURE",
372
- "CROPLAND_HIGH_WITHOUT_MANURE",
373
- "CROPLAND_MEDIUM",
374
- "CROPLAND_LOW",
375
- "OTHER"
376
- ])
377
- """
378
- Enum representing IPCC Carbon Input Categories for improved grasslands
379
- and annual croplands.
385
+ class IpccCarbonInputCategory(Enum):
386
+ """
387
+ Enum representing IPCC Carbon Input Categories for improved grasslands and annual croplands.
380
388
 
381
- See [IPCC (2019) Vol. 4, Ch. 4, 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html)
382
- for more information.
383
- """
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"
384
399
 
385
400
 
386
401
  # --- TIER 2 NAMED TUPLES FOR CARBON SOURCES AND MODEL RESULTS ---
@@ -449,33 +464,6 @@ annual_water_factors : list[float]
449
464
  """
450
465
 
451
466
 
452
- CarbonInputResult = NamedTuple(
453
- "CarbonInputResult",
454
- [
455
- ("timestamps", list[float]),
456
- ("organic_carbon_inputs", list[float]),
457
- ("average_nitrogen_contents", list[float]),
458
- ("average_lignin_contents", list[float]),
459
- ]
460
- )
461
- """
462
- A named tuple to hold the result of `_run_annual_organic_carbon_inputs`.
463
-
464
- Attributes
465
- ----------
466
- timestamps : list[int]
467
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
468
- organic_carbon_inputs : list[float]
469
- A list of organic carbon inputs to the soil for each year in the inventory, kg C ha-1.
470
- average_nitrogen_contents : list[float]
471
- A list of the average nitrogen contents of the carbon sources for each year in the inventory, decimal_proportion,
472
- kg N (kg d.m.)-1.
473
- average_lignin_contents : list[float]
474
- A list of the average lignin contents of the carbon sources for each year in the inventory, decimal_proportion,
475
- kg lignin (kg d.m.)-1.
476
- """
477
-
478
-
479
467
  Tier2SocResult = NamedTuple(
480
468
  "Tier2SocResult",
481
469
  [
@@ -523,45 +511,6 @@ carbon_input_factor : float
523
511
  The stock change factor for mineral soil organic C for the input of organic amendments, dimensionless.
524
512
  """
525
513
 
526
- CarbonInputArgs = NamedTuple("CarbonInputArgs", [
527
- ("num_grassland_improvements", int),
528
- ("has_irrigation", bool),
529
- ("has_residue_removed_or_burnt", bool),
530
- ("has_low_residue_producing_crops", bool),
531
- ("has_bare_fallow", bool),
532
- ("has_n_fixing_crop_or_inorganic_n_fertiliser_used", bool),
533
- ("has_practice_increasing_c_input", bool),
534
- ("has_cover_crop", bool),
535
- ("has_organic_fertiliser_or_soil_amendment_used", bool),
536
- ("has_animal_manure_used", bool)
537
- ])
538
- """
539
- Named tuple representing the arguments for the functions assigning `IpccCarbonInputCategories` to inventory years.
540
-
541
- Attributes
542
- ----------
543
- num_grassland_improvements : int
544
- The number of grassland improvements.
545
- has_irrigation : bool
546
- Indicates whether irrigation is applied to more than 30% of the site.
547
- has_residue_removed_or_burnt : bool
548
- Indicates whether residues are removed or burnt on more than 30% of the site.
549
- has_low_residue_producing_crops : bool
550
- Indicates whether low residue-producing crops are present on more than 70% of the site.
551
- has_bare_fallow : bool
552
- Indicates whether bare fallow is present on more than 30% of the site.
553
- has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
554
- Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
555
- has_practice_increasing_c_input : bool
556
- Indicates whether practices increasing carbon input are present on more than 30% of the site.
557
- has_cover_crop : bool
558
- Indicates whether cover crops are present on more than 30% of the site.
559
- has_organic_fertiliser_or_soil_amendment_used : bool
560
- Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
561
- has_animal_manure_used : bool
562
- Indicates whether animal manure is used on more than 30% of the site.
563
- """
564
-
565
514
 
566
515
  # --- SHARED TIER 1 & TIER 2 MAPPING DICTS ---
567
516
 
@@ -651,8 +600,8 @@ IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE = {
651
600
  IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: "Low-activity clay soils",
652
601
  }
653
602
  """
654
- A dictionary mapping IPCC soil categories to corresponding soil type and USDA soil type lookup values
655
- 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.
656
605
  """
657
606
 
658
607
  IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE = {
@@ -676,8 +625,8 @@ IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE = {
676
625
  IpccLandUseCategory.ANNUAL_CROPS: "Annual crops"
677
626
  }
678
627
  """
679
- A dictionary mapping IPCC land use categories to corresponding land cover lookup values
680
- 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.
681
630
  """
682
631
 
683
632
  IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID = {
@@ -688,8 +637,8 @@ IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID = {
688
637
  IpccManagementCategory.OTHER: "nativePasture"
689
638
  }
690
639
  """
691
- A dictionary mapping IPCC management categories to corresponding grassland management term IDs from the
692
- land cover glossary.
640
+ A dictionary mapping IPCC management categories to corresponding grassland management term IDs from the land cover
641
+ glossary.
693
642
  """
694
643
 
695
644
 
@@ -746,8 +695,7 @@ def _check_cycle_tillage_management_category(
746
695
  tillage_nodes,
747
696
  lookup=LOOKUP,
748
697
  target_lookup_values=target_lookup_values,
749
- cumulative_threshold=MIN_AREA_THRESHOLD,
750
- default_node_value=DEFAULT_NODE_VALUE
698
+ cumulative_threshold=MIN_AREA_THRESHOLD
751
699
  ) and (
752
700
  key is not IpccManagementCategory.NO_TILLAGE
753
701
  or _check_zero_tillages(tillage_nodes)
@@ -1516,18 +1464,16 @@ def _run_soc_stocks(
1516
1464
  annual_temperature_factors: list[float],
1517
1465
  annual_water_factors: list[float],
1518
1466
  annual_organic_carbon_inputs: list[float],
1519
- annual_average_nitrogen_contents_of_organic_carbon_sources: list[float],
1520
- annual_average_lignin_contents_of_organic_carbon_sources: list[float],
1467
+ annual_n_contents: list[float],
1468
+ annual_lignin_contents: list[float],
1521
1469
  annual_tillage_categories: Union[list[IpccManagementCategory], None] = None,
1522
1470
  sand_content: float = 0.33,
1523
1471
  run_in_period: int = 5,
1524
- initial_soc_stock: Union[float, None] = None,
1525
1472
  params: Union[dict[str, float], None] = None,
1526
1473
  ) -> Tier2SocResult:
1527
1474
  """
1528
1475
  Run the IPCC Tier 2 SOC model with precomputed `annual_temperature_factors`, `annual_water_factors`,
1529
- `annual_organic_carbon_inputs`, `annual_average_nitrogen_contents_of_organic_carbon_sources`,
1530
- `annual_average_lignin_contents_of_organic_carbon_sources`.
1476
+ `annual_organic_carbon_inputs`, `annual_n_contents`, `annual_lignin_contents`.
1531
1477
 
1532
1478
  Parameters
1533
1479
  ----------
@@ -1538,22 +1484,20 @@ def _run_soc_stocks(
1538
1484
  annual_water_factors : list[float]
1539
1485
  A list of water factors for each year in the inventory, dimensionless (see Equation 5.0F).
1540
1486
  annual_organic_carbon_inputs : list[float]
1541
- A list of organic carbon inputs to the soil for each year in the inventory, kg C ha-1 year-1
1542
- (see Equation 5.0H).
1543
- annual_average_nitrogen_contents_of_organic_carbon_sources : list[float]
1544
- A list of the average nitrogen contents of the organic carbon sources for each year in the inventory,
1545
- decimal proportion.
1546
- annual_average_lignin_contents_of_organic_carbon_sources : list[float]
1547
- A list of the average lignin contents of the organic carbon sources for each year in the inventory,
1548
- 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.
1549
1495
  annual_tillage_categories : list[IpccManagementCategory] | None
1550
1496
  A list of the site"s `IpccManagementCategory`s for each year in the inventory.
1551
1497
  sand_content : float
1552
1498
  The sand content of the site, decimal proportion, default value: `0.33`.
1553
1499
  run_in_period : int
1554
1500
  The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
1555
- initial_soc_stock : float | None
1556
- The measured or pre-computed initial SOC stock at the end of the run-in period, kg C ha-1.
1557
1501
  params : dict[str: float] | None
1558
1502
  Overrides for the model parameters. If `None` only default parameters will be used.
1559
1503
 
@@ -1598,29 +1542,16 @@ def _run_soc_stocks(
1598
1542
 
1599
1543
  # --- SPLIT ANNUAL DATA INTO RUN-IN AND INVENTORY PERIODS ---
1600
1544
 
1601
- inventory_temperature_factors = timeseries_to_inventory(
1602
- annual_temperature_factors, run_in_period
1603
- )
1604
- inventory_water_factors = timeseries_to_inventory(
1605
- annual_water_factors, run_in_period
1606
- )
1607
- inventory_carbon_inputs = timeseries_to_inventory(
1608
- annual_organic_carbon_inputs, run_in_period
1609
- )
1610
- inventory_nitrogen_contents = timeseries_to_inventory(
1611
- annual_average_nitrogen_contents_of_organic_carbon_sources, run_in_period
1612
- )
1613
- inventory_lignin_contents = timeseries_to_inventory(
1614
- annual_average_lignin_contents_of_organic_carbon_sources, run_in_period
1615
- )
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)
1616
1550
  inventory_f_2s = timeseries_to_inventory(annual_f_2s, run_in_period)
1617
- inventory_tillage_factors = timeseries_to_inventory(
1618
- annual_tillage_factors, run_in_period
1619
- )
1551
+ inventory_tillage_factors = timeseries_to_inventory(annual_tillage_factors, run_in_period)
1620
1552
 
1621
- inventory_timestamps = timestamps[
1622
- run_in_period - 1:
1623
- ] # 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:]
1624
1555
 
1625
1556
  # --- CALCULATE THE ACTIVE ACTIVE POOL STEADY STATES ---
1626
1557
 
@@ -1642,7 +1573,7 @@ def _run_soc_stocks(
1642
1573
  inventory_carbon_inputs,
1643
1574
  inventory_f_2s,
1644
1575
  inventory_lignin_contents,
1645
- inventory_nitrogen_contents,
1576
+ inventory_n_contents,
1646
1577
  )
1647
1578
  ]
1648
1579
 
@@ -1732,39 +1663,9 @@ def _run_soc_stocks(
1732
1663
 
1733
1664
  # --- CALCULATE THE ACTIVE, SLOW AND PASSIVE SOC STOCKS ---
1734
1665
 
1735
- init_total_steady_state = (
1736
- inventory_active_pool_steady_states[0] +
1737
- inventory_slow_pool_steady_states[0] + inventory_passive_pool_steady_states[0]
1738
- )
1739
-
1740
- init_active_frac = inventory_active_pool_steady_states[0]/init_total_steady_state
1741
- init_slow_frac = inventory_slow_pool_steady_states[0]/init_total_steady_state
1742
- init_passive_frac = 1 - (init_active_frac + init_slow_frac)
1743
-
1744
- inventory_active_pool_soc_stocks = []
1745
- inventory_slow_pool_soc_stocks = []
1746
- inventory_passive_pool_soc_stocks = []
1747
-
1748
- should_calc_run_in = initial_soc_stock is None
1749
-
1750
- inventory_active_pool_soc_stocks.insert(
1751
- 0,
1752
- inventory_active_pool_steady_states[0]
1753
- if should_calc_run_in
1754
- else init_active_frac * initial_soc_stock,
1755
- )
1756
- inventory_slow_pool_soc_stocks.insert(
1757
- 0,
1758
- inventory_slow_pool_steady_states[0]
1759
- if should_calc_run_in
1760
- else init_slow_frac * initial_soc_stock,
1761
- )
1762
- inventory_passive_pool_soc_stocks.insert(
1763
- 0,
1764
- inventory_passive_pool_steady_states[0]
1765
- if should_calc_run_in
1766
- else init_passive_frac * initial_soc_stock,
1767
- )
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]
1768
1669
 
1769
1670
  for index in range(1, len(inventory_timestamps), 1):
1770
1671
  inventory_active_pool_soc_stocks.insert(
@@ -1829,45 +1730,6 @@ def _check_12_months(inner_dict: dict, keys: set[Any]):
1829
1730
  # --- SUB-MODEL ANNUAL TEMPERATURE FACTORS ---
1830
1731
 
1831
1732
 
1832
- def _should_run_annual_temperature_factors(
1833
- site: dict
1834
- ) -> tuple[bool, dict]:
1835
- """
1836
- Extracts, formats and checks data from the site node to determine whether or not to run the annual temperature
1837
- factors model on a specific Hestia `Site`.
1838
-
1839
- Parameters
1840
- ----------
1841
- site : dict
1842
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
1843
-
1844
- Returns
1845
- -------
1846
- tuple[bool, dict]
1847
- `(should_run, grouped_data)`.
1848
- """
1849
- measurements = site.get("measurements", [])
1850
- temperature_monthly = find_term_match(measurements, TEMPERATURE_MONTHLY_TERM_ID, {})
1851
-
1852
- grouped_data = group_measurement_values_by_year(
1853
- temperature_monthly,
1854
- inner_key=_InnerKey.TEMPERATURES,
1855
- complete_years_only=True
1856
- )
1857
-
1858
- should_run = all([
1859
- all(
1860
- _check_12_months(inner, {_InnerKey.TEMPERATURES})
1861
- for inner in grouped_data.values()
1862
- ),
1863
- len(grouped_data.keys()) >= MIN_RUN_IN_PERIOD,
1864
- check_consecutive(grouped_data.keys())
1865
- ])
1866
-
1867
- logShouldRun(site, MODEL, TERM_ID, should_run, sub_model="_run_annual_temperature_factors")
1868
- return should_run, grouped_data
1869
-
1870
-
1871
1733
  def _run_annual_temperature_factors(
1872
1734
  timestamps: list[int],
1873
1735
  temperatures: list[list[float]],
@@ -1907,77 +1769,6 @@ def _run_annual_temperature_factors(
1907
1769
  # --- TIER 2 SUB-MODEL: ANNUAL WATER FACTORS ---
1908
1770
 
1909
1771
 
1910
- def _should_run_annual_water_factors(
1911
- site: dict,
1912
- cycles: list[dict]
1913
- ) -> tuple[bool, bool, dict]:
1914
- """
1915
- Extracts, formats and checks data from the site and cycle nodes to determine determine whether or not to run the
1916
- annual water factors model on a specific Hestia `Site` and `Cycle`s.
1917
-
1918
- TODO: Implement checks for monthly is_irrigateds from cycles.
1919
-
1920
- Parameters
1921
- ----------
1922
- site : dict
1923
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
1924
- cycles : list[dict]
1925
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
1926
-
1927
- Returns
1928
- -------
1929
- tuple[bool, bool, dict]
1930
- `(should_run, run_with_irrigation, grouped_data)`.
1931
- """
1932
- measurements = site.get("measurements", [])
1933
- precipitation_monthly = find_term_match(measurements, PRECIPITATION_MONTHLY_TERM_ID, {})
1934
- potential_evapotranspiration_monthly = find_term_match(measurements, PET_MONTHLY_TERM_ID, {})
1935
-
1936
- grouped_precipitations = group_measurement_values_by_year(
1937
- precipitation_monthly,
1938
- inner_key=_InnerKey.PRECIPITATIONS,
1939
- complete_years_only=True
1940
- )
1941
- grouped_pets = group_measurement_values_by_year(
1942
- potential_evapotranspiration_monthly,
1943
- inner_key=_InnerKey.PETS,
1944
- complete_years_only=True
1945
- )
1946
-
1947
- is_irrigateds = None # TODO: Implement is_irrigateds check.
1948
- run_with_irrigation = bool(is_irrigateds)
1949
-
1950
- grouped_data = (
1951
- merge(grouped_precipitations, grouped_pets) if is_irrigateds is None else reduce(
1952
- merge, [grouped_precipitations, grouped_pets, is_irrigateds]
1953
- )
1954
- )
1955
-
1956
- should_run = all([
1957
- all(
1958
- _check_12_months(inner, {_InnerKey.PRECIPITATIONS, _InnerKey.PETS})
1959
- for inner in grouped_data.values()
1960
- ),
1961
- not run_with_irrigation or all(
1962
- _check_12_months(inner, {_InnerKey.IS_IRRIGATEDS})
1963
- for inner in grouped_data.values()
1964
- ),
1965
- len(grouped_data.keys()) >= MIN_RUN_IN_PERIOD,
1966
- check_consecutive(grouped_data.keys()),
1967
- check_cycle_site_ids_identical(cycles)
1968
- ])
1969
-
1970
- logShouldRun(
1971
- site,
1972
- MODEL,
1973
- TERM_ID,
1974
- should_run,
1975
- sub_model="_run_annual_water_factors",
1976
- run_with_irrigation=run_with_irrigation
1977
- )
1978
- return should_run, run_with_irrigation, grouped_data
1979
-
1980
-
1981
1772
  def _run_annual_water_factors(
1982
1773
  timestamps: list[int],
1983
1774
  precipitations: list[list[float]],
@@ -2090,267 +1881,51 @@ def _get_carbon_sources_from_cycles(cycles: dict) -> list[CarbonSource]:
2090
1881
  return non_empty_list([_iterate_carbon_source(node) for node in inputs_and_products])
2091
1882
 
2092
1883
 
2093
- def _should_run_annual_organic_carbon_inputs(
2094
- site: dict,
2095
- cycles: list[dict]
2096
- ) -> tuple[bool, dict]:
2097
- """
2098
- Extracts, formats and checks data from the site node to determine whether or not to run the annual organic carbon
2099
- inputs model on a specific set of Hestia `Cycle`s.
2100
-
2101
- Parameters
2102
- ----------
2103
- cycles : list[dict]
2104
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
2105
-
2106
- Returns
2107
- -------
2108
- tuple[bool, dict]
2109
- `(should_run, grouped_data)`.
2110
- """
2111
- grouped_cycles = group_nodes_by_year(cycles)
2112
-
2113
- grouped_data = {
2114
- year: {
2115
- _InnerKey.CARBON_SOURCES: _get_carbon_sources_from_cycles(_cycles)
2116
- } for year, _cycles in grouped_cycles.items()
2117
- }
2118
-
2119
- should_run = all([
2120
- len(grouped_data.keys()) >= MIN_RUN_IN_PERIOD,
2121
- check_consecutive(grouped_data.keys()),
2122
- check_cycle_site_ids_identical(cycles)
2123
- ])
2124
-
2125
- logShouldRun(site, MODEL, TERM_ID, should_run, sub_model="_run_annual_organic_carbon_inputs")
2126
- return should_run, grouped_data
2127
-
2128
-
2129
- def _run_annual_organic_carbon_inputs(
2130
- timestamps: list[int],
2131
- annual_carbon_sources: list[list[CarbonSource]],
2132
- default_carbon_content: float = 0.42,
2133
- default_nitrogen_content: float = 0.0085,
2134
- default_lignin_content: float = 0.073,
2135
- ):
2136
- """
2137
- Calculate the organic carbon input, average nitrogen content of carbon sources and average lignin content of carbon
2138
- sources for each year of the inventory.
2139
-
2140
- `timestamps` and `annual_carbon_sources` must have the same length.
2141
-
2142
- Parameters
2143
- ----------
2144
- timestamps : list[int]
2145
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
2146
- annual_carbon_sources : list[list[CarbonSource]]
2147
- A list of carbon sources for each year of the inventory, where each carbon source is a named tupled with the
2148
- format `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`
2149
- default_carbon_content : float
2150
- The default carbon content of a carbon source, decimal proportion, kg C (kg d.m.)-1, default value: `0.42`.
2151
- default_nitrogen_content : float
2152
- The default nitrogen content of a carbon source, decimal proportion, kg N (kg d.m.)-1, default value: `0.0085`.
2153
- default_lignin_content : float)
2154
- The default lignin content of a carbon source, decimal proportion, kg lignin (kg d.m.)-1,
2155
- default value: `0.073`.
2156
-
2157
- Returns
2158
- -------
2159
- CarbonInputResult
2160
- An inventory of annual carbon input data as a named tuple with the format
2161
- `(timestamps: list[int], organic_carbon_inputs: list[float], average_nitrogen_contents: list[float],
2162
- average_lignin_contents: list[float])`
2163
- """
2164
- return CarbonInputResult(
2165
- timestamps=timestamps,
2166
- organic_carbon_inputs=[
2167
- _calc_total_organic_carbon_input(sources, default_carbon_content=default_carbon_content)
2168
- for sources in annual_carbon_sources
2169
- ],
2170
- average_nitrogen_contents=[
2171
- _calc_average_nitrogen_content_of_organic_carbon_sources(
2172
- sources, default_nitrogen_content=default_nitrogen_content)
2173
- for sources in annual_carbon_sources
2174
- ],
2175
- average_lignin_contents=[
2176
- _calc_average_lignin_content_of_organic_carbon_sources(
2177
- sources, default_lignin_content=default_lignin_content)
2178
- for sources in annual_carbon_sources
2179
- ],
2180
- )
2181
-
2182
-
2183
1884
  # --- TIER 2 SOC MODEL ---
2184
1885
 
2185
1886
 
2186
- def _should_run_tier_2(
2187
- site: dict
2188
- ) -> tuple:
2189
- """
2190
- Extracts, formats and checks data from the site and cycle nodes to determine
2191
- determine whether or not to run the Tier 2 SOC model.
2192
-
2193
- Parameters
2194
- ----------
2195
- site : dict
2196
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
2197
- cycles : list[dict]
2198
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
2199
-
2200
- Returns
2201
- -------
2202
- tuple
2203
- `(should_run, timestamps, temperatures, precipitations, pets, carbon_sources, tillage_categories, sand_content,
2204
- is_irrigateds, run_in_period, initial_soc_stock)`
2205
- """
2206
- cycles = related_cycles(site.get("@id"))
2207
-
2208
- should_run_t, grouped_temperature_data = _should_run_annual_temperature_factors(site)
2209
- should_run_w, run_with_irrigation, grouped_water_data = _should_run_annual_water_factors(site, cycles)
2210
- should_run_c, grouped_carbon_sources_data = _should_run_annual_organic_carbon_inputs(site, cycles)
2211
-
2212
- grouped_cycles = group_nodes_by_year(cycles)
2213
-
2214
- grouped_tillage_categories = {
2215
- year: {
2216
- _InnerKey.TILLAGE_CATEGORY: _assign_tier_2_ipcc_tillage_management_category(_cycles)
2217
- } for year, _cycles in grouped_cycles.items()
2218
- }
2219
-
2220
- # Combine all the grouped data into one dictionary
2221
- grouped_data = reduce(merge, [grouped_temperature_data, grouped_water_data,
2222
- grouped_carbon_sources_data, grouped_tillage_categories])
2223
-
2224
- # Select the correct keys for data completeness based on `run_with_irrigation`
2225
- keys = INNER_KEYS_RUN_WITH_IRRIGATION if run_with_irrigation else INNER_KEYS_RUN_WITHOUT_IRRIGATION
2226
-
2227
- # Filter out any incomplete years
2228
- complete_data = dict(filter(
2229
- lambda item: all([key in item[1].keys() for key in keys]),
2230
- grouped_data.items()
2231
- ))
2232
-
2233
- timestamps = list(complete_data)
2234
- start_year = timestamps[0] if timestamps else 0
2235
- end_year = timestamps[-1] if timestamps else 0
2236
-
2237
- measurements = site.get("measurements", [])
2238
-
2239
- sand_content_value, _ = most_relevant_measurement_value_by_depth_and_date(
2240
- measurements,
2241
- SAND_CONTENT_TERM_ID,
2242
- f"{start_year}-12-31",
2243
- DEPTH_UPPER,
2244
- DEPTH_LOWER,
2245
- depth_strict=False
2246
- ) if timestamps else (None, None)
2247
- sand_content = sand_content_value/100 if sand_content_value else None
2248
-
2249
- initial_soc_stock_value, initial_soc_stock_date = most_relevant_measurement_value_by_depth_and_date(
2250
- measurements,
2251
- TERM_ID,
2252
- f"{end_year}-12-31",
2253
- DEPTH_UPPER,
2254
- DEPTH_LOWER,
2255
- depth_strict=True
2256
- ) if timestamps else (None, None)
2257
-
2258
- run_with_initial_soc_stock = bool(initial_soc_stock_value and initial_soc_stock_date)
2259
-
2260
- run_in_period = (
2261
- int(abs(diff_in_years(f"{start_year}-12-31", initial_soc_stock_date)) + 1)
2262
- if run_with_initial_soc_stock else MIN_RUN_IN_PERIOD
2263
- ) if timestamps else 0
2264
-
2265
- timestamps = list(complete_data.keys())
2266
- temperatures = [complete_data[year][_InnerKey.TEMPERATURES] for year in timestamps]
2267
- precipitations = [complete_data[year][_InnerKey.PRECIPITATIONS] for year in timestamps]
2268
- pets = [complete_data[year][_InnerKey.PETS] for year in timestamps]
2269
- annual_carbon_sources = [complete_data[year][_InnerKey.CARBON_SOURCES] for year in timestamps]
2270
- annual_tillage_categories = [complete_data[year][_InnerKey.TILLAGE_CATEGORY] for year in timestamps]
2271
- is_irrigateds = (
2272
- [complete_data[year][_InnerKey.IS_IRRIGATEDS] for year in timestamps] if run_with_irrigation else None
2273
- )
2274
-
2275
- should_run = all([
2276
- should_run_t,
2277
- should_run_w,
2278
- should_run_c,
2279
- sand_content is not None and 0 < sand_content <= 1,
2280
- run_in_period >= MIN_RUN_IN_PERIOD,
2281
- len(timestamps) >= run_in_period,
2282
- check_cycle_site_ids_identical(cycles),
2283
- check_consecutive(timestamps)
2284
- ])
2285
-
2286
- logShouldRun(
2287
- site,
2288
- MODEL,
2289
- TERM_ID,
2290
- should_run,
2291
- sub_model="_run_tier_2",
2292
- run_with_irrigation=run_with_irrigation,
2293
- run_with_initial_soc_stock=run_with_initial_soc_stock,
2294
- run_in_period=run_in_period
2295
- )
2296
-
2297
- return (
2298
- should_run,
2299
- timestamps,
2300
- temperatures,
2301
- precipitations,
2302
- pets,
2303
- annual_carbon_sources,
2304
- annual_tillage_categories,
2305
- sand_content,
2306
- is_irrigateds,
2307
- run_in_period,
2308
- initial_soc_stock_value
2309
- )
2310
-
2311
-
2312
1887
  def _run_tier_2(
2313
- timestamps: list[int],
2314
- temperatures: list[list[float]],
2315
- precipitations: list[list[float]],
2316
- pets: list[list[float]],
2317
- annual_carbon_sources: list[list[CarbonSource]],
2318
- annual_tillage_categories: list[IpccManagementCategory],
2319
- sand_content: float = 0.33,
2320
- is_irrigateds: Union[list[list[bool]], None] = None,
1888
+ inventory: dict[int: dict[_InventoryKey: any]],
1889
+ *,
2321
1890
  run_in_period: int = 5,
2322
- initial_soc_stock: Union[float, None] = None,
1891
+ run_with_irrigation: bool = True,
1892
+ sand_content: float = 0.33,
2323
1893
  params: Union[dict[str, float], None] = None,
1894
+ **_
2324
1895
  ) -> list[dict]:
2325
1896
  """
2326
1897
  Run the IPCC Tier 2 SOC model on a time series of annual data about a site and the mangagement activities taking
2327
- place on it. `timestamps` and `annual_`... lists must be the same length.
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
+ ...
1915
+ }
1916
+ ```
1917
+
1918
+ TODO: interpolate between `sandContent` measurements for different years of the inventory
2328
1919
 
2329
1920
  Parameters
2330
1921
  ----------
2331
- timestamps : list[int]
2332
- A list of integer timestamps (e.g. [1995, 1996...]) for each year in the inventory.
2333
- temperatures : list[list[float]]
2334
- A list of monthly average temperatures for each year in the inventory.
2335
- precipitations : list[list[float]]
2336
- A list of monthly sum precipitations for each year in the inventory.
2337
- pets : list[list[float]]
2338
- A list of monthly sum potential evapotransiprations for each year in the inventory.
2339
- annual_carbon_sources : list[list[CarbonSource]]
2340
- A list of carbon sources for each year of the inventory, where each carbon source is a named tupled with the
2341
- format `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`
2342
- annual_tillage_categories : list[IpccManagementCategory)
2343
- A list of the site"s IpccManagementCategory for each year in the inventory.
2344
- sand_content : float
2345
- The sand content of the site, decimal proportion, default value: `0.33`.
2346
- is_irrigateds : list[list[bool]] | None
2347
- A list of monthly booleans that describe whether irrigation is used in a particular calendar month for each
2348
- year in the inventory.
2349
- run_in_period : int
1922
+ inventory : dict
1923
+ The inventory built by the `_should_run` function.
1924
+ run_in_period : int, optional
2350
1925
  The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
2351
- initial_soc_stock : float | None]
2352
- The measured or pre-computed initial SOC stock at the end of the run-in period, kg C ha-1.
2353
- params : dict | None
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
2354
1929
  Overrides for the model parameters. If `None` only default parameters will be used.
2355
1930
 
2356
1931
  Returns
@@ -2358,6 +1933,28 @@ def _run_tier_2(
2358
1933
  list[dict]
2359
1934
  A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
2360
1935
  """
1936
+ valid_inventory = {
1937
+ year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_2)
1938
+ }
1939
+
1940
+ timestamps = [year for year in valid_inventory.keys()]
1941
+
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()]
1945
+
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
+ )
1953
+
1954
+ sand_content = next(
1955
+ group[_InventoryKey.SAND_CONTENT]/100 for group in valid_inventory.values()
1956
+ if _InventoryKey.SAND_CONTENT in group
1957
+ )
2361
1958
 
2362
1959
  # --- MERGE ANY USER-SET PARAMETERS WITH THE IPCC DEFAULTS ---
2363
1960
 
@@ -2367,45 +1964,31 @@ def _run_tier_2(
2367
1964
 
2368
1965
  _, annual_temperature_factors = _run_annual_temperature_factors(
2369
1966
  timestamps,
2370
- temperatures,
1967
+ annual_temperature_monthlys,
2371
1968
  maximum_temperature=params.get("maximum_temperature"),
2372
1969
  optimum_temperature=params.get("optimum_temperature")
2373
1970
  )
2374
1971
 
2375
1972
  _, annual_water_factors = _run_annual_water_factors(
2376
1973
  timestamps,
2377
- precipitations,
2378
- pets,
2379
- is_irrigateds,
1974
+ annual_precipitation_monthlys,
1975
+ annual_pet_monthlys,
1976
+ annual_irrigated_monthly,
2380
1977
  water_factor_slope=params.get("water_factor_slope")
2381
1978
  )
2382
1979
 
2383
- (
2384
- _,
2385
- annual_organic_carbon_inputs,
2386
- annual_nitrogen_contents,
2387
- annual_lignin_contents
2388
- ) = _run_annual_organic_carbon_inputs(
2389
- timestamps,
2390
- annual_carbon_sources,
2391
- default_carbon_content=params.get("default_carbon_content"),
2392
- default_nitrogen_content=params.get("default_nitrogen_content"),
2393
- default_lignin_content=params.get("default_lignin_content")
2394
- )
2395
-
2396
1980
  # --- RUN THE MODEL ---
2397
1981
 
2398
1982
  result = _run_soc_stocks(
2399
1983
  timestamps=timestamps,
2400
1984
  annual_temperature_factors=annual_temperature_factors,
2401
1985
  annual_water_factors=annual_water_factors,
2402
- annual_organic_carbon_inputs=annual_organic_carbon_inputs,
2403
- annual_average_nitrogen_contents_of_organic_carbon_sources=annual_nitrogen_contents,
2404
- annual_average_lignin_contents_of_organic_carbon_sources=annual_lignin_contents,
1986
+ annual_organic_carbon_inputs=annual_carbon_inputs,
1987
+ annual_n_contents=annual_n_contents,
1988
+ annual_lignin_contents=annual_lignin_contents,
2405
1989
  annual_tillage_categories=annual_tillage_categories,
2406
1990
  sand_content=sand_content,
2407
1991
  run_in_period=run_in_period,
2408
- initial_soc_stock=initial_soc_stock,
2409
1992
  params=params
2410
1993
  )
2411
1994
 
@@ -2438,39 +2021,13 @@ def _run_tier_2(
2438
2021
  # --- TIER 1 FUNCTIONS ---
2439
2022
 
2440
2023
 
2441
- def _find_closest_value_index(
2442
- lst: Iterable[Union[int, float]], target_value: Union[int, float]
2443
- ) -> Optional[int]:
2024
+ def _retrieve_soc_ref(
2025
+ eco_climate_zone: int,
2026
+ ipcc_soil_category: IpccSoilCategory
2027
+ ) -> float:
2444
2028
  """
2445
- Find the index of the closest value in a list.
2446
-
2447
- Parameters
2448
- ----------
2449
- lst : iterable[int | float]
2450
- The list of integers.
2451
- target : int | float
2452
- The target value.
2453
-
2454
- Returns
2455
- -------
2456
- int
2457
- The index of the closest value.
2458
- """
2459
- should_run = all([
2460
- lst,
2461
- all(isinstance(element, (int, float)) for element in lst),
2462
- isinstance(target_value, (int, float))
2463
- ])
2464
- return min(range(len(lst)), key=lambda i: abs(lst[i] - target_value)) if should_run else None
2465
-
2466
-
2467
- def _retrieve_soc_ref(
2468
- eco_climate_zone: int,
2469
- ipcc_soil_category: IpccSoilCategory
2470
- ) -> float:
2471
- """
2472
- Retrieve the soil organic carbon (SOC) reference value for a given combination of eco-climate zone
2473
- and IPCC soil category.
2029
+ Retrieve the soil organic carbon (SOC) reference value for a given combination of eco-climate zone
2030
+ and IPCC soil category.
2474
2031
 
2475
2032
  See [IPCC (2019) Vol. 4, Ch. 2, Table 2.3](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html)
2476
2033
  for more information.
@@ -2500,9 +2057,8 @@ def _retrieve_soc_stock_factors(
2500
2057
  ipcc_carbon_input_category: IpccCarbonInputCategory
2501
2058
  ) -> StockChangeFactors:
2502
2059
  """
2503
- Retrieve the stock change factors for soil organic carbon (SOC)
2504
- based on a given combination of land use, management and carbon
2505
- 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.
2506
2062
 
2507
2063
  Parameters
2508
2064
  ----------
@@ -2562,8 +2118,8 @@ def _calc_soc_equilibrium(
2562
2118
  """
2563
2119
  Calculate the soil organic carbon (SOC) equilibrium based on reference SOC and factors.
2564
2120
 
2565
- In the tier 1 model, SOC equilibriums are considered to be reached after 20 years of
2566
- 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.
2567
2123
 
2568
2124
  Parameters
2569
2125
  ----------
@@ -2629,8 +2185,8 @@ def _iterate_soc_equilibriums(
2629
2185
  timestamps: list[int], soc_equilibriums: list[float]
2630
2186
  ) -> tuple[list[int], list[float]]:
2631
2187
  """
2632
- Iterate over SOC equilibriums, inserting timestamps and soc_equilibriums for any
2633
- 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.
2634
2190
 
2635
2191
  Parameters
2636
2192
  ----------
@@ -2670,7 +2226,6 @@ def _iterate_soc_equilibriums(
2670
2226
  )
2671
2227
 
2672
2228
  for index, (timestamp, soc_equilibrium) in enumerate(zip(timestamps, soc_equilibriums)):
2673
-
2674
2229
  equilibrium_reached_timestamp = calc_equilibrium_reached_timestamp(index)
2675
2230
 
2676
2231
  if is_missing_equilibrium_year(timestamp, equilibrium_reached_timestamp):
@@ -2691,8 +2246,8 @@ def _run_soc_equilibriums(
2691
2246
  """
2692
2247
  Run the soil organic carbon (SOC) equilibriums calculation for each year in the inventory.
2693
2248
 
2694
- Missing years where SOC equilibrium would be reached are inserted to allow for annual
2695
- 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.
2696
2251
 
2697
2252
  Parameters
2698
2253
  ----------
@@ -2714,8 +2269,8 @@ def _run_soc_equilibriums(
2714
2269
  Returns
2715
2270
  -------
2716
2271
  tuple[list[int], list[float]]
2717
- `timestamps` and `soc_equilibriums` for each year in the inventory, including any
2718
- 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.
2719
2274
  """
2720
2275
 
2721
2276
  # Calculate SOC equilibriums for each year
@@ -2748,8 +2303,7 @@ def _calc_tier_1_soc_stocks(
2748
2303
  soc_equilibriums: list[float],
2749
2304
  ) -> list[float]:
2750
2305
  """
2751
- Calculate soil organic carbon (SOC) stocks (kg C ha-1) in the 0-30cm depth interval for each year in
2752
- 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.
2753
2307
 
2754
2308
  Parameters
2755
2309
  ----------
@@ -2790,62 +2344,6 @@ def _calc_tier_1_soc_stocks(
2790
2344
  return soc_stocks
2791
2345
 
2792
2346
 
2793
- def _calc_measurement_scaling_factor(
2794
- measured_soc: float, calculated_soc: float
2795
- ) -> float:
2796
- """
2797
- Calculate the scaling factor soil organic carbon (SOC) values based
2798
- on the ratio between measured and calculated values.
2799
-
2800
- Parameters
2801
- ----------
2802
- measured_soc : float
2803
- The measured SOC value.
2804
- calculated_soc : float
2805
- The calculated SOC value.
2806
-
2807
- Returns
2808
- -------
2809
- float
2810
- The scaling factor.
2811
- """
2812
- return measured_soc / calculated_soc
2813
-
2814
-
2815
- def _scale_soc_stocks(
2816
- soc_stocks: list[float],
2817
- soc_measurement_value: Union[float, None] = None,
2818
- soc_measurement_index: Union[int, None] = None
2819
- ) -> list[float]:
2820
- """
2821
- Scale soil organic carbon (SOC) stocks based on a measurement value and index.
2822
-
2823
- Parameters
2824
- ----------
2825
- soc_stocks : list[float]
2826
- The list of SOC stocks to be scaled.
2827
- soc_measurement_value : float | None, optional
2828
- The measured SOC value for scaling. If None, no scaling is applied.
2829
- soc_measurement_index : int | None, optional
2830
- The index of the calculated SOC stock to compare against the SOC measurement.
2831
-
2832
- Returns
2833
- -------
2834
- list[float]
2835
- The scaled SOC stocks.
2836
- """
2837
-
2838
- measurement_scaling_factor = (
2839
- _calc_measurement_scaling_factor(
2840
- soc_measurement_value,
2841
- soc_stocks[soc_measurement_index]
2842
- ) if soc_measurement_value and soc_measurement_index is not None
2843
- else 1
2844
- )
2845
-
2846
- return [value * measurement_scaling_factor for value in soc_stocks]
2847
-
2848
-
2849
2347
  # --- GET THE ECO-CLIMATE ZONE FROM THE MEASUREMENTS ---
2850
2348
 
2851
2349
 
@@ -2864,7 +2362,6 @@ def _get_eco_climate_zone(measurements: list[dict]) -> Optional[int]:
2864
2362
  The eco-climate zone value if found, otherwise None.
2865
2363
  """
2866
2364
  eco_climate_zone = find_term_match(measurements, "ecoClimateZone")
2867
- # return measurement_value(eco_climate_zone) or None
2868
2365
  return get_node_value(eco_climate_zone) or None
2869
2366
 
2870
2367
 
@@ -2872,10 +2369,10 @@ def _get_eco_climate_zone(measurements: list[dict]) -> Optional[int]:
2872
2369
 
2873
2370
 
2874
2371
  def _check_soil_category(
2875
- soil_types: list[dict],
2876
- usda_soil_types: list[dict],
2877
2372
  *,
2878
2373
  key: IpccSoilCategory,
2374
+ soil_types: list[dict],
2375
+ usda_soil_types: list[dict],
2879
2376
  **_
2880
2377
  ) -> bool:
2881
2378
  """
@@ -2883,12 +2380,12 @@ def _check_soil_category(
2883
2380
 
2884
2381
  Parameters
2885
2382
  ----------
2383
+ key : IpccSoilCategory
2384
+ The IPCC soil category to check.
2886
2385
  soil_types : list[dict]
2887
2386
  List of soil type measurement nodes.
2888
2387
  usda_soil_types : list[dict]
2889
2388
  List of USDA soil type measurement nodes
2890
- key : IpccSoilCategory
2891
- The IPCC soil category to check.
2892
2389
 
2893
2390
  Returns
2894
2391
  -------
@@ -2904,26 +2401,25 @@ def _check_soil_category(
2904
2401
  soil_types,
2905
2402
  lookup=SOIL_TYPE_LOOKUP,
2906
2403
  target_lookup_values=target_lookup_values,
2907
- cumulative_threshold=MIN_AREA_THRESHOLD,
2908
- default_node_value=DEFAULT_NODE_VALUE
2404
+ cumulative_threshold=MIN_AREA_THRESHOLD
2909
2405
  )
2910
2406
 
2911
2407
  is_usda_soil_type_match = cumulative_nodes_lookup_match(
2912
2408
  usda_soil_types,
2913
2409
  lookup=USDA_SOIL_TYPE_LOOKUP,
2914
2410
  target_lookup_values=target_lookup_values,
2915
- cumulative_threshold=MIN_AREA_THRESHOLD,
2916
- default_node_value=DEFAULT_NODE_VALUE
2411
+ cumulative_threshold=MIN_AREA_THRESHOLD
2917
2412
  )
2918
2413
 
2919
2414
  return is_soil_type_match or is_usda_soil_type_match
2920
2415
 
2921
2416
 
2922
- def _check_sandy_soils(
2417
+ def _check_sandy_soil_category(
2418
+ *,
2419
+ key: IpccSoilCategory,
2923
2420
  soil_types: list[dict],
2924
2421
  usda_soil_types: list[dict],
2925
- *,
2926
- is_sandy: bool,
2422
+ has_sandy_soil: bool,
2927
2423
  **_
2928
2424
  ) -> bool:
2929
2425
  """
@@ -2933,11 +2429,13 @@ def _check_sandy_soils(
2933
2429
 
2934
2430
  Parameters
2935
2431
  ----------
2432
+ key : IpccSoilCategory
2433
+ The IPCC soil category to check.
2936
2434
  soil_types : list[dict]
2937
2435
  List of soil type measurement nodes.
2938
2436
  usda_soil_types : list[dict]
2939
2437
  List of USDA soil type measurement nodes
2940
- is_sandy : bool
2438
+ has_sandy_soil : bool
2941
2439
  True if the soils are sandy, False otherwise.
2942
2440
 
2943
2441
  Returns
@@ -2945,13 +2443,12 @@ def _check_sandy_soils(
2945
2443
  bool
2946
2444
  `True` if the soil category matches, `False` otherwise.
2947
2445
  """
2948
- KEY = IpccSoilCategory.SANDY_SOILS
2949
- 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
2950
2447
 
2951
2448
 
2952
2449
  SOIL_CATEGORY_DECISION_TREE = {
2953
2450
  IpccSoilCategory.ORGANIC_SOILS: _check_soil_category,
2954
- IpccSoilCategory.SANDY_SOILS: _check_sandy_soils,
2451
+ IpccSoilCategory.SANDY_SOILS: _check_sandy_soil_category,
2955
2452
  IpccSoilCategory.WETLAND_SOILS: _check_soil_category,
2956
2453
  IpccSoilCategory.VOLCANIC_SOILS: _check_soil_category,
2957
2454
  IpccSoilCategory.SPODIC_SOILS: _check_soil_category,
@@ -2990,19 +2487,17 @@ def _assign_ipcc_soil_category(
2990
2487
 
2991
2488
  clay_content = get_node_value(find_term_match(measurements, CLAY_CONTENT_TERM_ID))
2992
2489
  sand_content = get_node_value(find_term_match(measurements, SAND_CONTENT_TERM_ID))
2993
- # clay_content = measurement_value(find_term_match(measurements, CLAY_CONTENT_TERM_ID))
2994
- # sand_content = measurement_value(find_term_match(measurements, SAND_CONTENT_TERM_ID))
2995
2490
 
2996
- 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
2997
2492
 
2998
2493
  return next(
2999
2494
  (
3000
2495
  key for key in SOIL_CATEGORY_DECISION_TREE
3001
2496
  if SOIL_CATEGORY_DECISION_TREE[key](
3002
- soil_types,
3003
- usda_soil_types,
3004
2497
  key=key,
3005
- is_sandy=is_sandy
2498
+ soil_types=soil_types,
2499
+ usda_soil_types=usda_soil_types,
2500
+ has_sandy_soil=has_sandy_soil
3006
2501
  )
3007
2502
  ),
3008
2503
  default
@@ -3029,182 +2524,174 @@ def _has_irrigation(water_regime_nodes: list[dict]) -> bool:
3029
2524
  return cumulative_nodes_term_match(
3030
2525
  water_regime_nodes,
3031
2526
  target_term_ids=get_irrigated_terms(),
3032
- cumulative_threshold=MIN_AREA_THRESHOLD,
3033
- default_node_value=DEFAULT_NODE_VALUE
2527
+ cumulative_threshold=MIN_AREA_THRESHOLD
3034
2528
  )
3035
2529
 
3036
2530
 
3037
- def _check_ipcc_land_use_category(
3038
- site_type: str,
3039
- *,
3040
- key: IpccLandUseCategory,
3041
- **_
3042
- ):
2531
+ def _has_long_fallow(land_cover_nodes: list[dict]) -> bool:
3043
2532
  """
3044
- 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.
3045
2536
 
3046
2537
  Parameters
3047
2538
  ----------
3048
- site_type : str
3049
- The site type to check.
3050
- key : IpccLandUseCategory
3051
- The IPCC land use category to check.
2539
+ land_cover_nodes : List[dict]
2540
+ List of land cover nodes to be checked.
3052
2541
 
3053
2542
  Returns
3054
2543
  -------
3055
2544
  bool
3056
- `True` if the conditions match the specified land use category, `False` otherwise.
3057
-
2545
+ `True` if long fallow is present, `False` otherwise.
3058
2546
  """
3059
- target_site_type = IPCC_LAND_USE_CATEGORY_TO_SITE_TYPE.get(key, None)
3060
-
3061
- 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
+ )
3062
2556
 
3063
2557
 
3064
- def _check_cropland_land_use_category(
3065
- site_type: str,
3066
- *,
3067
- key: IpccLandUseCategory,
3068
- land_cover_nodes: list[dict],
3069
- **_
3070
- ) -> bool:
2558
+ def _has_upland_rice(land_cover_nodes: list[dict]) -> bool:
3071
2559
  """
3072
- Check if the site type and land cover nodes match the target conditions for a cropland IpccLandUseCategory.
3073
-
3074
- This function is special case of `_check_cropland_land_use_category`.
2560
+ Check if upland rice is present in the land cover nodes.
3075
2561
 
3076
2562
  Parameters
3077
2563
  ----------
3078
- site_type : str
3079
- The site type to check.
3080
- key : IpccLandUseCategory
3081
- The IPCC land use category to check.
3082
- land_cover_nodes : list[dict]
3083
- List of land cover nodes.
2564
+ land_cover_nodes : List[dict]
2565
+ List of land cover nodes to be checked.
3084
2566
 
3085
2567
  Returns
3086
2568
  -------
3087
2569
  bool
3088
- `True` if the conditions match the specified land use category, `False` otherwise.
2570
+ `True` if upland rice is present, `False` otherwise.
3089
2571
  """
3090
- LOOKUP = LOOKUPS["landCover"][0]
3091
- target_lookup_values = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(key, None)
3092
-
3093
- return _check_ipcc_land_use_category(site_type, key=key) and cumulative_nodes_lookup_match(
2572
+ return cumulative_nodes_term_match(
3094
2573
  land_cover_nodes,
3095
- lookup=LOOKUP,
3096
- target_lookup_values=target_lookup_values,
3097
- cumulative_threshold=MIN_AREA_THRESHOLD,
3098
- default_node_value=DEFAULT_NODE_VALUE
2574
+ target_term_ids=get_upland_rice_land_cover_terms(),
2575
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
3099
2576
  )
3100
2577
 
3101
2578
 
3102
- def _check_paddy_rice_cultivation_category(
3103
- site_type: str,
3104
- *,
3105
- land_cover_nodes: list[dict],
3106
- is_irrigated_upland_rice: bool,
3107
- **_
3108
- ) -> bool:
3109
- """
3110
- 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
+ """
3111
2586
 
3112
- 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.
3113
2591
 
3114
2592
  Parameters
3115
2593
  ----------
2594
+ key : IpccLandUseCategory
2595
+ The IPCC land use category to check.
3116
2596
  site_type : str
3117
2597
  The site type to check.
3118
- land_cover_nodes : list[dict]
3119
- List of land cover nodes.
3120
- is_irrigated_upland_rice : bool
3121
- 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.
3122
2605
 
3123
2606
  Returns
3124
2607
  -------
3125
2608
  bool
3126
2609
  `True` if the conditions match the specified land use category, `False` otherwise.
2610
+
3127
2611
  """
3128
- KEY = IpccLandUseCategory.PADDY_RICE_CULTIVATION
3129
- return _check_cropland_land_use_category(
3130
- site_type, key=KEY, land_cover_nodes=land_cover_nodes
3131
- ) 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
3132
2616
 
3133
2617
 
3134
- def _check_annual_crops_category(
3135
- site_type: str,
3136
- *,
3137
- land_cover_nodes: list[dict],
3138
- has_long_fallow: bool,
3139
- **_
2618
+ def _check_cropland_land_use_category(
2619
+ *, key: IpccLandUseCategory, site_type: str, land_cover_nodes: list[dict], **kwargs
3140
2620
  ) -> bool:
3141
2621
  """
3142
- 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.
3143
2623
 
3144
- This function is special case of `_check_cropland_land_use_category`.
2624
+ This function is special case of `_check_ipcc_land_use_category`.
3145
2625
 
3146
2626
  Parameters
3147
2627
  ----------
2628
+ key : IpccLandUseCategory
2629
+ The IPCC land use category to check.
3148
2630
  site_type : str
3149
2631
  The site type to check.
3150
- land_cover_nodes : list[dict]
3151
- List of land cover nodes.
2632
+
2633
+ Keyword Args
2634
+ ------------
3152
2635
  has_long_fallow : bool
3153
- 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.
3154
2639
 
3155
2640
  Returns
3156
2641
  -------
3157
2642
  bool
3158
2643
  `True` if the conditions match the specified land use category, `False` otherwise.
3159
2644
  """
3160
- KEY = IpccLandUseCategory.ANNUAL_CROPS
3161
- return _check_cropland_land_use_category(
3162
- site_type, key=KEY, land_cover_nodes=land_cover_nodes
3163
- ) 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
3164
2654
 
3165
2655
 
3166
- def _check_annual_crops_wet_category(
3167
- site_type: str,
3168
- *,
3169
- land_cover_nodes: list[dict],
3170
- has_wetland_soils: bool,
3171
- has_long_fallow: bool,
3172
- **_
3173
- ):
2656
+ def _check_paddy_rice_cultivation_land_use_category(
2657
+ *, key: IpccLandUseCategory, site_type: str, has_irrigated_upland_rice: bool, **kwargs
2658
+ ) -> bool:
3174
2659
  """
3175
- 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.
3176
2661
 
3177
2662
  This function is special case of `_check_cropland_land_use_category`.
3178
2663
 
3179
2664
  Parameters
3180
2665
  ----------
2666
+ key : IpccLandUseCategory
2667
+ The IPCC land use category to check.
3181
2668
  site_type : str
3182
2669
  The site type to check.
3183
- land_cover_nodes : list[dict]
3184
- List of land cover nodes.
3185
- has_wetland_soils : bool
3186
- 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.
3187
2675
  has_long_fallow : bool
3188
- 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.
3189
2679
 
3190
2680
  Returns
3191
2681
  -------
3192
2682
  bool
3193
2683
  `True` if the conditions match the specified land use category, `False` otherwise.
3194
2684
  """
3195
- KEY = IpccLandUseCategory.ANNUAL_CROPS_WET
3196
- return _check_cropland_land_use_category(
3197
- site_type, key=KEY, land_cover_nodes=land_cover_nodes
3198
- ) 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
3199
2686
 
3200
2687
 
3201
2688
  LAND_USE_CATEGORY_DECISION_TREE = {
3202
2689
  IpccLandUseCategory.GRASSLAND: _check_ipcc_land_use_category,
3203
- IpccLandUseCategory.PERENNIAL_CROPS: _check_cropland_land_use_category,
3204
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: _check_paddy_rice_cultivation_category,
3205
- IpccLandUseCategory.ANNUAL_CROPS_WET: _check_annual_crops_wet_category,
3206
- IpccLandUseCategory.ANNUAL_CROPS: _check_annual_crops_category,
3207
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,
3208
2695
  IpccLandUseCategory.FOREST: _check_ipcc_land_use_category,
3209
2696
  IpccLandUseCategory.NATIVE: _check_ipcc_land_use_category,
3210
2697
  IpccLandUseCategory.OTHER: _check_ipcc_land_use_category
@@ -3219,9 +2706,7 @@ and land cover nodes.
3219
2706
 
3220
2707
 
3221
2708
  def _assign_ipcc_land_use_category(
3222
- site_type: str,
3223
- management_nodes: list[dict],
3224
- ipcc_soil_category: IpccSoilCategory
2709
+ site_type: str, management_nodes: list[dict], ipcc_soil_category: IpccSoilCategory
3225
2710
  ) -> IpccLandUseCategory:
3226
2711
  """
3227
2712
  Assigns IPCC land use category based on site type, management nodes, and soil category.
@@ -3243,33 +2728,13 @@ def _assign_ipcc_land_use_category(
3243
2728
  DECISION_TREE = LAND_USE_CATEGORY_DECISION_TREE
3244
2729
  DEFAULT = IpccLandUseCategory.OTHER
3245
2730
 
3246
- land_cover_nodes = filter_list_term_type(
3247
- management_nodes, [TermTermType.LANDCOVER]
3248
- )
3249
-
3250
- water_regime_nodes = filter_list_term_type(
3251
- management_nodes, [TermTermType.WATERREGIME]
3252
- )
2731
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
2732
+ water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
3253
2733
 
3254
2734
  has_irrigation = _has_irrigation(water_regime_nodes)
3255
-
3256
- is_upland_rice = cumulative_nodes_term_match(
3257
- land_cover_nodes,
3258
- target_term_ids=get_rice_plant_upland_terms(),
3259
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD,
3260
- default_node_value=DEFAULT_NODE_VALUE
3261
- )
3262
-
3263
- is_irrigated_upland_rice = is_upland_rice and has_irrigation
3264
-
3265
- # SUPER_MAJORITY_AREA_THRESHOLD
3266
- has_long_fallow = cumulative_nodes_match(
3267
- lambda node: get_node_property(node, LONG_FALLOW_CROP_TERM_ID, False).get("value", 0),
3268
- land_cover_nodes,
3269
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD,
3270
- default_node_value=DEFAULT_NODE_VALUE
3271
- )
3272
-
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)
3273
2738
  has_wetland_soils = ipcc_soil_category is IpccSoilCategory.WETLAND_SOILS
3274
2739
 
3275
2740
  should_run = bool(site_type)
@@ -3278,11 +2743,11 @@ def _assign_ipcc_land_use_category(
3278
2743
  (
3279
2744
  key for key in DECISION_TREE
3280
2745
  if DECISION_TREE[key](
3281
- site_type,
3282
2746
  key=key,
2747
+ site_type=site_type,
3283
2748
  land_cover_nodes=land_cover_nodes,
3284
- is_irrigated_upland_rice=is_irrigated_upland_rice,
3285
2749
  has_long_fallow=has_long_fallow,
2750
+ has_irrigated_upland_rice=has_irrigated_upland_rice,
3286
2751
  has_wetland_soils=has_wetland_soils
3287
2752
  )
3288
2753
  ),
@@ -3294,20 +2759,17 @@ def _assign_ipcc_land_use_category(
3294
2759
 
3295
2760
 
3296
2761
  def _check_grassland_ipcc_management_category(
3297
- *,
3298
- land_cover_nodes: list[dict],
3299
- key: IpccManagementCategory,
3300
- **_
2762
+ *, key: IpccManagementCategory, land_cover_nodes: list[dict], **_
3301
2763
  ) -> bool:
3302
2764
  """
3303
2765
  Check if the land cover nodes match the target conditions for a grassland IpccManagementCategory.
3304
2766
 
3305
2767
  Parameters
3306
2768
  ----------
3307
- land_cover_nodes : List[dict]
3308
- List of land cover nodes to be checked.
3309
2769
  key : IpccManagementCategory
3310
2770
  The IPCC management category to check.
2771
+ land_cover_nodes : List[dict]
2772
+ List of land cover nodes to be checked.
3311
2773
 
3312
2774
  Returns
3313
2775
  -------
@@ -3315,25 +2777,25 @@ def _check_grassland_ipcc_management_category(
3315
2777
  `True` if the conditions match the specified management category, `False` otherwise.
3316
2778
  """
3317
2779
  target_term_id = IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID.get(key, None)
3318
-
3319
2780
  return cumulative_nodes_term_match(
3320
2781
  land_cover_nodes,
3321
2782
  target_term_ids=target_term_id,
3322
- cumulative_threshold=MIN_AREA_THRESHOLD,
3323
- default_node_value=DEFAULT_NODE_VALUE
2783
+ cumulative_threshold=MIN_AREA_THRESHOLD
3324
2784
  )
3325
2785
 
3326
2786
 
3327
- 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:
3328
2790
  """
3329
2791
  Check if the tillage nodes match the target conditions for a tillage IpccManagementCategory.
3330
2792
 
3331
2793
  Parameters
3332
2794
  ----------
3333
- tillage_nodes : List[dict]
3334
- List of tillage nodes to be checked.
3335
2795
  key : IpccManagementCategory
3336
2796
  The IPCC management category to check.
2797
+ tillage_nodes : List[dict]
2798
+ List of tillage nodes to be checked.
3337
2799
 
3338
2800
  Returns
3339
2801
  -------
@@ -3342,13 +2804,11 @@ def _check_tillage_ipcc_management_category(*, tillage_nodes: list[dict], key: I
3342
2804
  """
3343
2805
  LOOKUP = LOOKUPS["tillage"]
3344
2806
  target_lookup_values = IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE.get(key, None)
3345
-
3346
2807
  return cumulative_nodes_lookup_match(
3347
2808
  tillage_nodes,
3348
2809
  lookup=LOOKUP,
3349
2810
  target_lookup_values=target_lookup_values,
3350
- cumulative_threshold=MIN_AREA_THRESHOLD,
3351
- default_node_value=DEFAULT_NODE_VALUE
2811
+ cumulative_threshold=MIN_AREA_THRESHOLD
3352
2812
  )
3353
2813
 
3354
2814
 
@@ -3404,8 +2864,7 @@ Value: Default IPCC management category for the given land use category.
3404
2864
 
3405
2865
 
3406
2866
  def _assign_ipcc_management_category(
3407
- management_nodes: list[dict],
3408
- ipcc_land_use_category: IpccLandUseCategory
2867
+ management_nodes: list[dict], ipcc_land_use_category: IpccLandUseCategory
3409
2868
  ) -> IpccManagementCategory:
3410
2869
  """
3411
2870
  Assign an IPCC Management Category based on the given management nodes and IPCC Land Use Category.
@@ -3422,32 +2881,26 @@ def _assign_ipcc_management_category(
3422
2881
  IpccManagementCategory
3423
2882
  The assigned IPCC Management Category.
3424
2883
  """
3425
- decision_tree = IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE.get(
3426
- ipcc_land_use_category, {}
3427
- )
2884
+ decision_tree = IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE.get(ipcc_land_use_category, {})
3428
2885
  default = IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY.get(
3429
2886
  ipcc_land_use_category, IpccManagementCategory.OTHER
3430
2887
  )
3431
2888
 
3432
- land_cover_nodes = filter_list_term_type(
3433
- management_nodes, [TermTermType.LANDCOVER]
3434
- )
3435
- tillage_nodes = filter_list_term_type(
3436
- management_nodes, [TermTermType.TILLAGE]
3437
- )
2889
+ land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
2890
+ tillage_nodes = filter_list_term_type(management_nodes, [TermTermType.TILLAGE])
3438
2891
 
3439
- should_run = (
3440
- len(land_cover_nodes) > 0 if decision_tree == GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE
3441
- else len(tillage_nodes) > 0
3442
- )
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
+ ])
3443
2896
 
3444
2897
  return next(
3445
2898
  (
3446
2899
  key for key in decision_tree
3447
2900
  if decision_tree[key](
2901
+ key=key,
3448
2902
  land_cover_nodes=land_cover_nodes,
3449
2903
  tillage_nodes=tillage_nodes,
3450
- key=key
3451
2904
  )
3452
2905
  ),
3453
2906
  default
@@ -3470,9 +2923,7 @@ Value: Minimum number of improvements required for the corresponding Grassland C
3470
2923
 
3471
2924
 
3472
2925
  def _check_grassland_ipcc_carbon_input_category(
3473
- carbon_input_args: CarbonInputArgs,
3474
- *,
3475
- key: IpccCarbonInputCategory
2926
+ *, key: IpccCarbonInputCategory, num_grassland_improvements: int, **_,
3476
2927
  ) -> bool:
3477
2928
  """
3478
2929
  Checks if the given carbon input arguments satisfy the conditions for a specific
@@ -3480,24 +2931,26 @@ def _check_grassland_ipcc_carbon_input_category(
3480
2931
 
3481
2932
  Parameters
3482
2933
  ----------
3483
- carbon_input_args : CarbonInputArgs
3484
- The carbon input arguments.
3485
2934
  key : IpccCarbonInputCategory
3486
2935
  The grassland IPCC Carbon Input Category to check.
2936
+ num_grassland_improvements : int
2937
+ The number of grassland improvements.
3487
2938
 
3488
2939
  Returns
3489
2940
  -------
3490
2941
  bool
3491
2942
  `True` if the conditions for the specified category are met; otherwise, `False`.
3492
2943
  """
3493
- min_improvements = (
3494
- GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS[key]
3495
- )
3496
- return carbon_input_args.num_grassland_improvements >= min_improvements
2944
+ return num_grassland_improvements >= GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS[key]
3497
2945
 
3498
2946
 
3499
2947
  def _check_cropland_high_with_manure_category(
3500
- 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,
3501
2954
  **_
3502
2955
  ) -> Optional[int]:
3503
2956
  """
@@ -3505,8 +2958,16 @@ def _check_cropland_high_with_manure_category(
3505
2958
 
3506
2959
  Parameters
3507
2960
  ----------
3508
- carbon_input_args : CarbonInputArgs
3509
- 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.
3510
2971
 
3511
2972
  Returns
3512
2973
  -------
@@ -3515,11 +2976,11 @@ def _check_cropland_high_with_manure_category(
3515
2976
  """
3516
2977
  conditions = {
3517
2978
  1: all([
3518
- not carbon_input_args.has_residue_removed_or_burnt,
3519
- not carbon_input_args.has_low_residue_producing_crops,
3520
- not carbon_input_args.has_bare_fallow,
3521
- carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3522
- 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
3523
2984
  ])
3524
2985
  }
3525
2986
 
@@ -3529,15 +2990,41 @@ def _check_cropland_high_with_manure_category(
3529
2990
 
3530
2991
 
3531
2992
  def _check_cropland_high_without_manure_category(
3532
- 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
+ **_
3533
3004
  ) -> Optional[int]:
3534
3005
  """
3535
3006
  Checks the Cropland High without Manure IPCC Carbon Input Category based on the given carbon input arguments.
3536
3007
 
3537
3008
  Parameters
3538
3009
  ----------
3539
- carbon_input_args : CarbonInputArgs
3540
- 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.
3541
3028
 
3542
3029
  Returns
3543
3030
  -------
@@ -3546,17 +3033,17 @@ def _check_cropland_high_without_manure_category(
3546
3033
  """
3547
3034
  conditions = {
3548
3035
  1: all([
3549
- not carbon_input_args.has_residue_removed_or_burnt,
3550
- not carbon_input_args.has_low_residue_producing_crops,
3551
- not carbon_input_args.has_bare_fallow,
3552
- 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,
3553
3040
  any([
3554
- carbon_input_args.has_irrigation,
3555
- carbon_input_args.has_practice_increasing_c_input,
3556
- carbon_input_args.has_cover_crop,
3557
- 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
3558
3045
  ]),
3559
- not carbon_input_args.has_animal_manure_used
3046
+ not has_animal_manure_used
3560
3047
  ])
3561
3048
  }
3562
3049
 
@@ -3566,7 +3053,16 @@ def _check_cropland_high_without_manure_category(
3566
3053
 
3567
3054
 
3568
3055
  def _check_cropland_medium_category(
3569
- 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,
3570
3066
  **_
3571
3067
  ) -> Optional[int]:
3572
3068
  """
@@ -3574,8 +3070,24 @@ def _check_cropland_medium_category(
3574
3070
 
3575
3071
  Parameters
3576
3072
  ----------
3577
- carbon_input_args : CarbonInputArgs
3578
- 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.
3579
3091
 
3580
3092
  Returns
3581
3093
  -------
@@ -3584,43 +3096,43 @@ def _check_cropland_medium_category(
3584
3096
  """
3585
3097
  conditions = {
3586
3098
  1: all([
3587
- carbon_input_args.has_residue_removed_or_burnt,
3588
- carbon_input_args.has_animal_manure_used
3099
+ has_residue_removed_or_burnt,
3100
+ has_animal_manure_used
3589
3101
  ]),
3590
3102
  2: all([
3591
- not carbon_input_args.has_residue_removed_or_burnt,
3103
+ not has_residue_removed_or_burnt,
3592
3104
  any([
3593
- carbon_input_args.has_low_residue_producing_crops,
3594
- carbon_input_args.has_bare_fallow
3105
+ has_low_residue_producing_crops,
3106
+ has_bare_fallow
3595
3107
  ]),
3596
3108
  any([
3597
- carbon_input_args.has_irrigation,
3598
- carbon_input_args.has_practice_increasing_c_input,
3599
- carbon_input_args.has_cover_crop,
3600
- 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,
3601
3113
  ])
3602
3114
  ]),
3603
3115
  3: all([
3604
- not carbon_input_args.has_residue_removed_or_burnt,
3605
- not carbon_input_args.has_low_residue_producing_crops,
3606
- not carbon_input_args.has_bare_fallow,
3607
- 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,
3608
3120
  any([
3609
- carbon_input_args.has_irrigation,
3610
- carbon_input_args.has_practice_increasing_c_input,
3611
- carbon_input_args.has_cover_crop,
3612
- 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
3613
3125
  ])
3614
3126
  ]),
3615
3127
  4: all([
3616
- not carbon_input_args.has_residue_removed_or_burnt,
3617
- not carbon_input_args.has_low_residue_producing_crops,
3618
- not carbon_input_args.has_bare_fallow,
3619
- carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3620
- not carbon_input_args.has_irrigation,
3621
- not carbon_input_args.has_organic_fertiliser_or_soil_amendment_used,
3622
- not carbon_input_args.has_practice_increasing_c_input,
3623
- 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
3624
3136
  ])
3625
3137
  }
3626
3138
 
@@ -3630,7 +3142,16 @@ def _check_cropland_medium_category(
3630
3142
 
3631
3143
 
3632
3144
  def _check_cropland_low_category(
3633
- 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,
3634
3155
  **_
3635
3156
  ) -> Optional[int]:
3636
3157
  """
@@ -3638,8 +3159,24 @@ def _check_cropland_low_category(
3638
3159
 
3639
3160
  Parameters
3640
3161
  ----------
3641
- carbon_input_args : CarbonInputArgs
3642
- 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.
3643
3180
 
3644
3181
  Returns
3645
3182
  -------
@@ -3648,29 +3185,29 @@ def _check_cropland_low_category(
3648
3185
  """
3649
3186
  conditions = {
3650
3187
  1: all([
3651
- carbon_input_args.has_residue_removed_or_burnt,
3652
- not carbon_input_args.has_animal_manure_used
3188
+ has_residue_removed_or_burnt,
3189
+ not has_animal_manure_used
3653
3190
  ]),
3654
3191
  2: all([
3655
- not carbon_input_args.has_residue_removed_or_burnt,
3192
+ not has_residue_removed_or_burnt,
3656
3193
  any([
3657
- carbon_input_args.has_low_residue_producing_crops,
3658
- carbon_input_args.has_bare_fallow
3194
+ has_low_residue_producing_crops,
3195
+ has_bare_fallow
3659
3196
  ]),
3660
- not carbon_input_args.has_irrigation,
3661
- not carbon_input_args.has_practice_increasing_c_input,
3662
- not carbon_input_args.has_cover_crop,
3663
- 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
3664
3201
  ]),
3665
3202
  3: all([
3666
- not carbon_input_args.has_residue_removed_or_burnt,
3667
- not carbon_input_args.has_low_residue_producing_crops,
3668
- not carbon_input_args.has_bare_fallow,
3669
- not carbon_input_args.has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3670
- not carbon_input_args.has_irrigation,
3671
- not carbon_input_args.has_organic_fertiliser_or_soil_amendment_used,
3672
- not carbon_input_args.has_practice_increasing_c_input,
3673
- 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
3674
3211
  ])
3675
3212
  }
3676
3213
 
@@ -3679,9 +3216,9 @@ def _check_cropland_low_category(
3679
3216
  )
3680
3217
 
3681
3218
 
3682
- def _make_carbon_input_args(
3219
+ def _get_carbon_input_kwargs(
3683
3220
  management_nodes: list[dict]
3684
- ) -> CarbonInputArgs:
3221
+ ) -> dict:
3685
3222
  """
3686
3223
  Creates CarbonInputArgs based on the provided list of management nodes.
3687
3224
 
@@ -3692,8 +3229,8 @@ def _make_carbon_input_args(
3692
3229
 
3693
3230
  Returns
3694
3231
  -------
3695
- CarbonInputArgs
3696
- The carbon input arguments.
3232
+ dict
3233
+ The carbon input keyword arguments.
3697
3234
  """
3698
3235
 
3699
3236
  PRACTICE_INCREASING_C_INPUT_LOOKUP = LOOKUPS["landUseManagement"]
@@ -3708,62 +3245,56 @@ def _make_carbon_input_args(
3708
3245
  ORGANIC_FERTILISER_USED_TERM_ID
3709
3246
  }
3710
3247
 
3711
- crop_residue_management_nodes = filter_list_term_type(
3712
- management_nodes, [TermTermType.CROPRESIDUEMANAGEMENT]
3713
- )
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])
3714
3252
 
3715
- land_cover_nodes = filter_list_term_type(
3716
- management_nodes, [TermTermType.LANDCOVER]
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)
3717
3255
  )
3718
3256
 
3719
- land_use_management_nodes = filter_list_term_type(
3720
- management_nodes, [TermTermType.LANDUSEMANAGEMENT]
3721
- )
3722
-
3723
- water_regime_nodes = filter_list_term_type(
3724
- management_nodes, [TermTermType.WATERREGIME]
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
3725
3261
  )
3726
3262
 
3727
- has_irrigation = _has_irrigation(water_regime_nodes)
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
3269
+ )
3728
3270
 
3729
- has_residue_removed_or_burnt = cumulative_nodes_term_match(
3730
- crop_residue_management_nodes,
3731
- target_term_ids=get_residue_removed_or_burnt_terms(),
3732
- cumulative_threshold=MIN_AREA_THRESHOLD,
3733
- default_node_value=DEFAULT_NODE_VALUE
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)
3734
3274
  )
3735
3275
 
3276
+ has_irrigation = _has_irrigation(water_regime_nodes)
3277
+
3736
3278
  # SUPER_MAJORITY_AREA_THRESHOLD
3737
3279
  has_low_residue_producing_crops = cumulative_nodes_lookup_match(
3738
3280
  land_cover_nodes,
3739
3281
  lookup=LOW_RESIDUE_PRODUCING_CROP_LOOKUP,
3740
3282
  target_lookup_values=True,
3741
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD,
3742
- default_node_value=DEFAULT_NODE_VALUE
3743
- )
3744
-
3745
- has_bare_fallow = cumulative_nodes_term_match(
3746
- land_use_management_nodes,
3747
- target_term_ids=SHORT_BARE_FALLOW_TERM_ID,
3748
- cumulative_threshold=MIN_AREA_THRESHOLD,
3749
- default_node_value=DEFAULT_NODE_VALUE
3283
+ cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
3750
3284
  )
3751
3285
 
3752
3286
  has_n_fixing_crop = cumulative_nodes_lookup_match(
3753
3287
  land_cover_nodes,
3754
3288
  lookup=N_FIXING_CROP_LOOKUP,
3755
3289
  target_lookup_values=True,
3756
- cumulative_threshold=MIN_AREA_THRESHOLD,
3757
- default_node_value=DEFAULT_NODE_VALUE
3290
+ cumulative_threshold=MIN_AREA_THRESHOLD
3758
3291
  )
3759
3292
 
3760
- has_inorganic_n_fertiliser_used = any(
3761
- get_node_value(node) for node in land_use_management_nodes
3762
- if node_term_match(node, INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID)
3763
- )
3293
+ has_n_fixing_crop_or_inorganic_n_fertiliser_used = has_n_fixing_crop or has_inorganic_n_fertiliser_used
3764
3294
 
3765
- has_n_fixing_crop_or_inorganic_n_fertiliser_used = (
3766
- 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)
3767
3298
  )
3768
3299
 
3769
3300
  has_practice_increasing_c_input = cumulative_nodes_match(
@@ -3772,27 +3303,13 @@ def _make_carbon_input_args(
3772
3303
  and not node_term_match(node, EXCLUDED_PRACTICE_TERM_IDS)
3773
3304
  ),
3774
3305
  land_use_management_nodes,
3775
- cumulative_threshold=MIN_AREA_THRESHOLD,
3776
- default_node_value=DEFAULT_NODE_VALUE
3777
- )
3778
-
3779
- has_cover_crop = cumulative_nodes_match(
3780
- lambda node: any(
3781
- get_node_property(node, term_id, False).get("value", False) for term_id in get_cover_crop_property_terms()
3782
- ),
3783
- land_cover_nodes,
3784
- cumulative_threshold=MIN_AREA_THRESHOLD,
3785
- default_node_value=DEFAULT_NODE_VALUE
3786
- )
3787
-
3788
- has_organic_fertiliser_or_soil_amendment_used = any(
3789
- get_node_value(node) for node in land_use_management_nodes
3790
- if node_term_match(node, ORGANIC_FERTILISER_USED_TERM_ID)
3306
+ cumulative_threshold=MIN_AREA_THRESHOLD
3791
3307
  )
3792
3308
 
3793
- has_animal_manure_used = any(
3794
- get_node_value(node) for node in land_use_management_nodes
3795
- 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
3796
3313
  )
3797
3314
 
3798
3315
  num_grassland_improvements = [
@@ -3802,18 +3319,18 @@ def _make_carbon_input_args(
3802
3319
  has_organic_fertiliser_or_soil_amendment_used
3803
3320
  ].count(True)
3804
3321
 
3805
- return CarbonInputArgs(
3806
- num_grassland_improvements=num_grassland_improvements,
3807
- has_irrigation=has_irrigation,
3808
- has_residue_removed_or_burnt=has_residue_removed_or_burnt,
3809
- has_low_residue_producing_crops=has_low_residue_producing_crops,
3810
- has_bare_fallow=has_bare_fallow,
3811
- has_n_fixing_crop_or_inorganic_n_fertiliser_used=has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3812
- has_practice_increasing_c_input=has_practice_increasing_c_input,
3813
- has_cover_crop=has_cover_crop,
3814
- has_organic_fertiliser_or_soil_amendment_used=has_organic_fertiliser_or_soil_amendment_used,
3815
- has_animal_manure_used=has_animal_manure_used
3816
- )
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
+ }
3817
3334
 
3818
3335
 
3819
3336
  GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE = {
@@ -3889,14 +3406,12 @@ def _assign_ipcc_carbon_input_category(
3889
3406
  decision_tree = DECISION_TREE_FROM_IPCC_MANAGEMENT_CATEGORY.get(ipcc_management_category, {})
3890
3407
  default = DEFAULT_CARBON_INPUT_CATEGORY.get(ipcc_management_category, IpccCarbonInputCategory.OTHER)
3891
3408
 
3892
- carbon_input_args = _make_carbon_input_args(management_nodes)
3893
-
3894
3409
  should_run = len(management_nodes) > 0
3895
3410
 
3896
3411
  return next(
3897
3412
  (key for key in decision_tree if decision_tree[key](
3898
- carbon_input_args,
3899
3413
  key=key,
3414
+ **_get_carbon_input_kwargs(management_nodes)
3900
3415
  )),
3901
3416
  default
3902
3417
  ) if should_run else default
@@ -3905,50 +3420,477 @@ def _assign_ipcc_carbon_input_category(
3905
3420
  # --- TIER 1 SOC MODEL ---
3906
3421
 
3907
3422
 
3908
- 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]:
3909
3430
  """
3910
- 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.
3911
3434
 
3912
- 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
+ ```
3913
3449
 
3914
3450
  Parameters
3915
3451
  ----------
3916
- inner_dict : dict
3917
- 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.
3918
3460
 
3919
3461
  Returns
3920
3462
  -------
3921
- bool
3922
- 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.
3923
3465
  """
3924
- REQUIRED_KEYS = {IpccLandUseCategory, IpccManagementCategory, IpccCarbonInputCategory}
3925
- return (
3926
- inner_dict[IpccLandUseCategory] != IpccLandUseCategory.OTHER
3927
- 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
3483
+ )
3484
+
3485
+ soc_stocks = _calc_tier_1_soc_stocks(iterated_timestamps, iterated_soc_equilibriums)
3486
+
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
+ ```
3538
+ """
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)
3928
3591
  )
3929
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
3930
3597
 
3931
- def _should_run_tier_1(site: dict) -> tuple:
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.
3932
3608
  """
3933
- Determines whether Tier 1 of the IPCC SOC model should run for a given site.
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.
3934
3732
 
3935
3733
  Parameters
3936
3734
  ----------
3937
- site : dict
3938
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
3735
+ group : dict
3736
+ Dictionary containing information for a specific inventory year.
3939
3737
 
3940
3738
  Returns
3941
3739
  -------
3942
- tuple
3943
- A tuple containing information to determine if Tier 1 should run, including timestamps,
3944
- IPCC land use categories, management categories, carbon input categories, eco-climate zone,
3945
- SOC reference, and initial SOC stock.
3946
- """
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
+ )
3947
3752
 
3948
- site_type = site.get("siteType", "")
3949
- management_nodes = site.get("management", [])
3950
- measurement_nodes = site.get("measurements", [])
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
+ ])
3758
+
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 ---
3951
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
+ """
3952
3894
  eco_climate_zone = _get_eco_climate_zone(measurement_nodes)
3953
3895
  ipcc_soil_category = _assign_ipcc_soil_category(measurement_nodes)
3954
3896
  soc_ref = _retrieve_soc_ref(eco_climate_zone, ipcc_soil_category)
@@ -3957,7 +3899,7 @@ def _should_run_tier_1(site: dict) -> tuple:
3957
3899
 
3958
3900
  grouped_land_use_categories = {
3959
3901
  year: {
3960
- IpccLandUseCategory: _assign_ipcc_land_use_category(
3902
+ _InventoryKey.LU_CATEGORY: _assign_ipcc_land_use_category(
3961
3903
  site_type,
3962
3904
  nodes,
3963
3905
  ipcc_soil_category
@@ -3967,171 +3909,72 @@ def _should_run_tier_1(site: dict) -> tuple:
3967
3909
 
3968
3910
  grouped_management_categories = {
3969
3911
  year: {
3970
- IpccManagementCategory: _assign_ipcc_management_category(
3912
+ _InventoryKey.MG_CATEGORY: _assign_ipcc_management_category(
3971
3913
  nodes,
3972
- grouped_land_use_categories[year][IpccLandUseCategory]
3914
+ grouped_land_use_categories[year][_InventoryKey.LU_CATEGORY]
3973
3915
  )
3974
3916
  } for year, nodes in grouped_management.items()
3975
3917
  }
3976
3918
 
3977
3919
  grouped_carbon_input_categories = {
3978
3920
  year: {
3979
- IpccCarbonInputCategory: _assign_ipcc_carbon_input_category(
3921
+ _InventoryKey.CI_CATEGORY: _assign_ipcc_carbon_input_category(
3980
3922
  nodes,
3981
- grouped_management_categories[year][IpccManagementCategory]
3923
+ grouped_management_categories[year][_InventoryKey.MG_CATEGORY]
3982
3924
  )
3983
3925
  } for year, nodes in grouped_management.items()
3984
3926
  }
3985
3927
 
3986
- grouped_data = reduce(merge, [
3928
+ grouped_data = merge(
3987
3929
  grouped_land_use_categories,
3988
3930
  grouped_management_categories,
3989
3931
  grouped_carbon_input_categories
3990
- ])
3932
+ )
3991
3933
 
3992
- complete_data = {
3993
- year: inner_dict for year, inner_dict in grouped_data.items()
3994
- if _should_run_inventory_year(inner_dict)
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()
3995
3937
  }
3996
3938
 
3997
- timestamps = list(complete_data)
3998
- start_year = timestamps[0] if timestamps else None
3999
- end_year = timestamps[-1] if timestamps else None
4000
-
4001
- soc_measurement_value, soc_measurement_date = most_relevant_measurement_value_by_depth_and_date(
4002
- measurement_nodes,
4003
- TERM_ID,
4004
- f"{end_year}-12-31", # Last year in inventory
4005
- DEPTH_UPPER,
4006
- DEPTH_LOWER,
4007
- depth_strict=True
4008
- ) if end_year else (None, None)
4009
-
4010
- soc_measurement_datetime = safe_parse_date(soc_measurement_date)
4011
- soc_measurement_year = (
4012
- soc_measurement_datetime.year if soc_measurement_datetime else None
4013
- )
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
+ }
4014
3945
 
4015
- ipcc_land_use_categories = [complete_data[year][IpccLandUseCategory] for year in timestamps]
4016
- ipcc_management_categories = [complete_data[year][IpccManagementCategory] for year in timestamps]
4017
- ipcc_carbon_input_categories = [complete_data[year][IpccCarbonInputCategory] for year in timestamps]
3946
+ return inventory, kwargs
4018
3947
 
4019
- should_run = all([
4020
- soc_ref > 0,
4021
- isinstance(eco_climate_zone, int) and eco_climate_zone > 0,
4022
- len(timestamps) > 0,
4023
- (
4024
- start_year <= soc_measurement_year < end_year
4025
- ) if soc_measurement_value and soc_measurement_year else True
4026
- ])
4027
3948
 
4028
- logShouldRun(
4029
- site, MODEL, TERM_ID, should_run,
4030
- timestamps=log_as_table(set(timestamps)),
4031
- ipcc_land_use_categories=log_as_table(set(ipcc_land_use_categories)),
4032
- ipcc_management_categories=log_as_table(set(ipcc_management_categories)),
4033
- ipcc_carbon_input_categories=log_as_table(set(ipcc_carbon_input_categories)),
4034
- eco_climate_zone=eco_climate_zone,
4035
- soc_ref=soc_ref,
4036
- run_with_soc_measurement=(soc_measurement_value and soc_measurement_year),
4037
- soc_measurement_value=soc_measurement_value,
4038
- soc_measurement_year=soc_measurement_year
4039
- )
4040
-
4041
- return (
4042
- should_run,
4043
- timestamps,
4044
- ipcc_land_use_categories,
4045
- ipcc_management_categories,
4046
- ipcc_carbon_input_categories,
4047
- eco_climate_zone,
4048
- soc_ref,
4049
- soc_measurement_value,
4050
- soc_measurement_year
4051
- )
4052
-
4053
-
4054
- def _run_tier_1(
4055
- timestamps: list[int],
4056
- ipcc_land_use_categories: list[IpccLandUseCategory],
4057
- ipcc_management_categories: list[IpccManagementCategory],
4058
- ipcc_carbon_input_categories: list[IpccCarbonInputCategory],
4059
- eco_climate_zone: int,
4060
- soc_ref: float,
4061
- soc_measurement_value: Optional[float] = None,
4062
- soc_measurement_year: Optional[int] = None
4063
- ) -> list[dict]:
3949
+ def _should_run_inventory_year_tier_1(group: dict) -> bool:
4064
3950
  """
4065
- Run the IPCC (2019) Tier 1 methodology for calculating SOC stocks (in kg C ha-1) for each year in the inventory
4066
- 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.
4067
3952
 
4068
- 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.
4069
3955
 
4070
3956
  Parameters
4071
3957
  ----------
4072
- timestamps : list[int]
4073
- A list of timestamps for each year in the inventory.
4074
- ipcc_land_use_categories : list[IpccLandUseCategory]
4075
- A list of IPCC land use categories for each year in the inventory.
4076
- ipcc_management_categories : list[IpccManagementCategory]
4077
- A list of IPCC management categories for each year in the inventory.
4078
- ipcc_carbon_input_categories : list[IpccCarbonInputCategory]
4079
- A list of IPCC carbon input categories for each year in the inventory.
4080
- eco_climate_zone : int
4081
- The eco-climate zone identifier for the site corresponding to a row in the
4082
- [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
4083
- lookup table.
4084
- ipcc_soil_category : IpccSoilCategory
4085
- The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
4086
- soc_measurement_value : float | None, optional
4087
- Measured SOC stock, if provided.
4088
- soc_measurement_year : int | None, optional
4089
- The year the SOC measurement took place, if provided.
3958
+ group : dict
3959
+ Dictionary containing information for a specific inventory year.
4090
3960
 
4091
3961
  Returns
4092
3962
  -------
4093
- tuple
3963
+ bool
3964
+ True if the inventory year is valid, False otherwise.
4094
3965
  """
4095
-
4096
- iterated_timestamps, iterated_soc_equilibriums = _run_soc_equilibriums(
4097
- timestamps,
4098
- ipcc_land_use_categories,
4099
- ipcc_management_categories,
4100
- ipcc_carbon_input_categories,
4101
- eco_climate_zone,
4102
- soc_ref
4103
- )
4104
-
4105
- soc_stocks = _calc_tier_1_soc_stocks(iterated_timestamps, iterated_soc_equilibriums)
4106
-
4107
- soc_measurement_index = _find_closest_value_index(iterated_timestamps, soc_measurement_year)
4108
-
4109
- scaled_soc_stocks = _scale_soc_stocks(
4110
- soc_stocks, soc_measurement_value, soc_measurement_index
4111
- )
4112
-
4113
- slice_index = soc_measurement_index or 0
4114
- result_timestamps = iterated_timestamps[slice_index:]
4115
- result_soc_stocks = scaled_soc_stocks[slice_index:]
4116
-
4117
- return [
4118
- _measurement(
4119
- year,
4120
- soc_stock,
4121
- MeasurementMethodClassification.TIER_1_MODEL.value
4122
- ) for year, soc_stock in zip(
4123
- result_timestamps,
4124
- result_soc_stocks
4125
- )
4126
- ]
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
+ ])
4127
3970
 
4128
3971
 
4129
- # --- SHARED TIER 1 & TIER 2 RUN FUNCTION ---
3972
+ # --- RUN ---
4130
3973
 
4131
3974
 
4132
3975
  def run(site: dict) -> list[dict]:
4133
3976
  """
4134
- 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.
4135
3978
 
4136
3979
  Parameters
4137
3980
  ----------
@@ -4143,13 +3986,9 @@ def run(site: dict) -> list[dict]:
4143
3986
  list[dict]
4144
3987
  A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
4145
3988
  """
4146
- should_run_tier_2, *tier_2_args = _should_run_tier_2(site)
4147
- should_run_tier_1, *tier_1_args = (
4148
- _should_run_tier_1(site) if not should_run_tier_2
4149
- else (False, None)
4150
- )
3989
+ should_run_tier_1, should_run_tier_2, inventory, kwargs = _should_run(site)
4151
3990
  return (
4152
- _run_tier_2(*tier_2_args) if should_run_tier_2
4153
- 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
4154
3993
  else []
4155
3994
  )