hestia-earth-models 0.61.7__py3-none-any.whl → 0.62.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (51) hide show
  1. hestia_earth/models/cycle/completeness/electricityFuel.py +60 -0
  2. hestia_earth/models/cycle/product/economicValueShare.py +47 -31
  3. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +44 -59
  4. hestia_earth/models/geospatialDatabase/histosol.py +4 -0
  5. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +4 -2
  6. hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +1 -1
  7. hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +1 -1
  8. hestia_earth/models/ipcc2019/animal/pastureGrass.py +30 -24
  9. hestia_earth/models/ipcc2019/belowGroundCropResidue.py +1 -1
  10. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -1
  11. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +511 -458
  12. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +5 -1
  13. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +116 -3882
  14. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2060 -0
  15. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1630 -0
  16. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +324 -0
  17. hestia_earth/models/ipcc2019/pastureGrass.py +37 -19
  18. hestia_earth/models/ipcc2019/pastureGrass_utils.py +4 -21
  19. hestia_earth/models/mocking/search-results.json +293 -289
  20. hestia_earth/models/site/organicCarbonPerHa.py +58 -44
  21. hestia_earth/models/site/soilMeasurement.py +18 -13
  22. hestia_earth/models/utils/__init__.py +28 -0
  23. hestia_earth/models/utils/array_builders.py +578 -0
  24. hestia_earth/models/utils/blank_node.py +55 -39
  25. hestia_earth/models/utils/descriptive_stats.py +285 -0
  26. hestia_earth/models/utils/emission.py +73 -2
  27. hestia_earth/models/utils/inorganicFertiliser.py +2 -2
  28. hestia_earth/models/utils/measurement.py +118 -4
  29. hestia_earth/models/version.py +1 -1
  30. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/METADATA +2 -2
  31. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/RECORD +51 -39
  32. tests/models/cycle/completeness/test_electricityFuel.py +21 -0
  33. tests/models/cycle/product/test_economicValueShare.py +8 -0
  34. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +2 -2
  35. tests/models/ipcc2019/animal/test_pastureGrass.py +2 -2
  36. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +55 -165
  37. tests/models/ipcc2019/test_organicCarbonPerHa.py +219 -460
  38. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +471 -0
  39. tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +208 -0
  40. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +75 -0
  41. tests/models/ipcc2019/test_pastureGrass.py +0 -16
  42. tests/models/site/test_organicCarbonPerHa.py +3 -12
  43. tests/models/site/test_soilMeasurement.py +3 -18
  44. tests/models/utils/test_array_builders.py +253 -0
  45. tests/models/utils/test_blank_node.py +154 -15
  46. tests/models/utils/test_descriptive_stats.py +134 -0
  47. tests/models/utils/test_emission.py +51 -1
  48. tests/models/utils/test_measurement.py +54 -2
  49. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/LICENSE +0 -0
  50. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/WHEEL +0 -0
  51. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.62.0.dist-info}/top_level.txt +0 -0
@@ -1,70 +1,39 @@
1
1
  """
2
2
  The IPCC model for estimating soil organic carbon stock changes in the 0 - 30cm depth interval due to management
3
- changes. This model combines the Tier 1 & Tier 2 methodologies. It first tries to run Tier 2 (only on croplands
4
- remaining croplands). If Tier 2 cannot run, it will try to run Tier 1 (for croplands remaining croplands and for
5
- grasslands remaining grasslands).
3
+ changes. This model will attempt to run the Tier 2 methodology and return results. If the Tier 2 methodology cannot be
4
+ run, it will attempt to run the Tier 1 methodology and return the results.
6
5
 
7
- More information on this model, including data requirements **and** recommendations, tier methodologies, and examples,
8
- can be found in the
9
- [Hestia SOC wiki](https://gitlab.com/hestia-earth/hestia-engine-models/-/wikis/Soil-organic-carbon-modelling).
6
+ Both tier methodologies are run as Monte Carlo simulations with 10000 iterations, allowing the model to calculate a
7
+ `value` (mean), `sd`, `min`, `max` for each year of the model result.
10
8
 
11
- Source:
12
- [IPCC 2019, Vol. 4, Chapter 10](https://www.ipcc-nggip.iges.or.jp/public/2019rf/pdf/4_Volume4/19R_V4_Ch05_Cropland.pdf).
9
+ The requirements in this file are for the Tier 1 methodology only, as it has simpler requirements. The requirements for
10
+ the Tier 2 methodology can be found in the
11
+ [Hestia SOC wiki](https://gitlab.com/hestia-earth/hestia-engine-models/-/wikis/Soil-organic-carbon-modelling)
12
+ alongside data recommendations, examples and explanations for both tiers.
13
13
  """
14
- from enum import Enum
15
- from numpy import exp
14
+ from functools import reduce
16
15
  from pydash.objects import merge
17
- from statistics import mean
18
- from typing import (
19
- Any,
20
- NamedTuple,
21
- Optional,
22
- Union
23
- )
24
- from hestia_earth.schema import (
25
- CycleFunctionalUnit,
26
- MeasurementMethodClassification,
27
- SiteSiteType,
28
- TermTermType,
29
- )
30
- from hestia_earth.utils.model import find_term_match, filter_list_term_type
31
- from hestia_earth.utils.tools import flatten, list_sum, non_empty_list
16
+ from types import ModuleType
32
17
 
33
18
  from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
34
- from hestia_earth.models.utils.blank_node import (
35
- cumulative_nodes_match,
36
- cumulative_nodes_lookup_match,
37
- cumulative_nodes_term_match,
38
- get_node_value,
39
- group_nodes_by_year_and_month,
40
- group_nodes_by_year,
41
- GroupNodesByYearMode,
42
- node_lookup_match,
43
- node_term_match
44
- )
45
- from hestia_earth.models.utils.cycle import check_cycle_site_ids_identical
46
- from hestia_earth.models.utils.ecoClimateZone import get_ecoClimateZone_lookup_value
47
- from hestia_earth.models.utils.measurement import (
48
- _new_measurement,
49
- )
50
- from hestia_earth.models.utils.property import get_node_property
51
- from hestia_earth.models.utils.term import (
52
- get_cover_crop_property_terms,
53
- get_crop_residue_incorporated_or_left_on_field_terms,
54
- get_irrigated_terms,
55
- get_residue_removed_or_burnt_terms,
56
- get_upland_rice_crop_terms,
57
- get_upland_rice_land_cover_terms
58
- )
59
- from hestia_earth.models.utils.site import related_cycles
60
- from .utils import check_consecutive
61
- from . import MODEL
19
+
20
+ from .organicCarbonPerHa_utils import format_bool, format_bool_list, format_enum, format_number, format_number_list
21
+ from . import organicCarbonPerHa_tier_1_utils as tier_1
22
+ from . import organicCarbonPerHa_tier_2_utils as tier_2
23
+ from . import MODEL # noqa
62
24
 
63
25
  REQUIREMENTS = {
64
26
  "Site": {
65
27
  "siteType": ["cropland", "permanent pasture", "forest", "other natural vegetation"],
28
+ "management": [
29
+ {"@type": "Management", "value": "", "term.termType": "landCover"}
30
+ ],
66
31
  "measurements": [
67
- {"@type": "Measurement", "value": "", "term.@id": "ecoClimateZone"}
32
+ {
33
+ "@type": "Measurement",
34
+ "value": ["1", "2", "3", "4", "7", "8", "9", "10", "11", "12"],
35
+ "term.@id": "ecoClimateZone"
36
+ }
68
37
  ],
69
38
  "optional": {
70
39
  "measurements": [
@@ -79,7 +48,6 @@ REQUIREMENTS = {
79
48
  "term.termType": "cropResidueManagement",
80
49
  "name": ["burnt", "removed"]
81
50
  },
82
- {"@type": "Management", "value": "", "startDate": "", "endDate": "", "term.termType": "landCover"},
83
51
  {
84
52
  "@type": "Management",
85
53
  "value": "",
@@ -94,29 +62,29 @@ REQUIREMENTS = {
94
62
  "startDate": "",
95
63
  "endDate": "",
96
64
  "term.termType": "waterRegime",
97
- "name": ["irrigated", "deep water"]
65
+ "name": ["deep water", "irrigated"]
98
66
  },
99
- {"@type": "Management", "value": "", "startDate": "", "endDate": "", "term.@id": "animalManureUsed"},
100
67
  {
101
68
  "@type": "Management",
102
69
  "value": "",
103
70
  "startDate": "",
104
71
  "endDate": "",
105
- "term.@id": "inorganicNitrogenFertiliserUsed"
72
+ "term.@id": "amendmentIncreasingSoilCarbonUsed"
106
73
  },
74
+ {"@type": "Management", "value": "", "startDate": "", "endDate": "", "term.@id": "animalManureUsed"},
107
75
  {
108
76
  "@type": "Management",
109
77
  "value": "",
110
78
  "startDate": "",
111
79
  "endDate": "",
112
- "term.@id": "organicFertiliserUsed"
80
+ "term.@id": "inorganicNitrogenFertiliserUsed"
113
81
  },
114
82
  {
115
83
  "@type": "Management",
116
84
  "value": "",
117
85
  "startDate": "",
118
86
  "endDate": "",
119
- "term.@id": "amendmentIncreasingSoilCarbonUsed"
87
+ "term.@id": "organicFertiliserUsed"
120
88
  },
121
89
  {"@type": "Management", "value": "", "startDate": "", "endDate": "", "term.@id": "shortBareFallow"}
122
90
  ]
@@ -125,33 +93,6 @@ REQUIREMENTS = {
125
93
  }
126
94
  LOOKUPS = {
127
95
  "crop": "IPCC_LAND_USE_CATEGORY",
128
- "ecoClimateZone": [
129
- "IPCC_2019_SOC_REF_KG_C_HECTARE_SAN",
130
- "IPCC_2019_SOC_REF_KG_C_HECTARE_WET",
131
- "IPCC_2019_SOC_REF_KG_C_HECTARE_VOL",
132
- "IPCC_2019_SOC_REF_KG_C_HECTARE_POD",
133
- "IPCC_2019_SOC_REF_KG_C_HECTARE_HAC",
134
- "IPCC_2019_SOC_REF_KG_C_HECTARE_LAC",
135
- "IPCC_2019_LANDUSE_FACTOR_GRASSLAND",
136
- "IPCC_2019_LANDUSE_FACTOR_PERENNIAL_CROPS",
137
- "IPCC_2019_LANDUSE_FACTOR_PADDY_RICE_CULTIVATION",
138
- "IPCC_2019_LANDUSE_FACTOR_ANNUAL_CROPS_WET",
139
- "IPCC_2019_LANDUSE_FACTOR_ANNUAL_CROPS",
140
- "IPCC_2019_LANDUSE_FACTOR_SET_ASIDE",
141
- "IPCC_2019_GRASSLAND_MANAGEMENT_FACTOR_SEVERELY_DEGRADED",
142
- "IPCC_2019_GRASSLAND_MANAGEMENT_FACTOR_IMPROVED_GRASSLAND",
143
- "IPCC_2019_GRASSLAND_MANAGEMENT_FACTOR_HIGH_INTENSITY_GRAZING",
144
- "IPCC_2019_GRASSLAND_MANAGEMENT_FACTOR_NOMINALLY_MANAGED",
145
- "IPCC_2019_TILLAGE_MANAGEMENT_FACTOR_FULL_TILLAGE",
146
- "IPCC_2019_TILLAGE_MANAGEMENT_FACTOR_REDUCED_TILLAGE",
147
- "IPCC_2019_TILLAGE_MANAGEMENT_FACTOR_NO_TILLAGE",
148
- "IPCC_2019_GRASSLAND_CARBON_INPUT_FACTOR_HIGH",
149
- "IPCC_2019_GRASSLAND_CARBON_INPUT_FACTOR_MEDIUM",
150
- "IPCC_2019_CROPLAND_CARBON_INPUT_FACTOR_HIGH_WITH_MANURE",
151
- "IPCC_2019_CROPLAND_CARBON_INPUT_FACTOR_HIGH_WITHOUT_MANURE",
152
- "IPCC_2019_CROPLAND_CARBON_INPUT_FACTOR_MEDIUM",
153
- "IPCC_2019_CROPLAND_CARBON_INPUT_FACTOR_LOW"
154
- ],
155
96
  "landCover": [
156
97
  "IPCC_LAND_USE_CATEGORY",
157
98
  "LOW_RESIDUE_PRODUCING_CROP",
@@ -165,3838 +106,131 @@ LOOKUPS = {
165
106
  RETURNS = {
166
107
  "Measurement": [{
167
108
  "value": "",
109
+ "sd": "",
110
+ "min": "",
111
+ "max": "",
112
+ "statsDefinition": "simulated",
113
+ "observations": "",
168
114
  "dates": "",
169
115
  "depthUpper": "0",
170
116
  "depthLower": "30",
171
117
  "methodClassification": ""
172
118
  }]
173
119
  }
174
-
175
120
  TERM_ID = 'organicCarbonPerHa'
121
+ ITERATIONS = 10000 # TODO: Refine number of iterations to balance performance and precision.
176
122
 
177
- # --- SHARED TIER 1 & TIER 2 CONSTANTS ---
178
-
179
- MIN_AREA_THRESHOLD = 30 # 30% as per IPCC guidelines
180
- SUPER_MAJORITY_AREA_THRESHOLD = 100 - MIN_AREA_THRESHOLD
181
- MIN_YIELD_THRESHOLD = 1
182
- DEPTH_UPPER = 0
183
- DEPTH_LOWER = 30
184
-
185
- # --- TIER 2 CONSTANTS ---
186
-
187
- NUMBER_OF_TILLAGES_TERM_ID = "numberOfTillages"
188
- TEMPERATURE_MONTHLY_TERM_ID = "temperatureMonthly"
189
- PRECIPITATION_MONTHLY_TERM_ID = "precipitationMonthly"
190
- PET_MONTHLY_TERM_ID = "potentialEvapotranspirationMonthly"
191
- SAND_CONTENT_TERM_ID = "sandContent"
192
- CARBON_CONTENT_TERM_ID = "carbonContent"
193
- NITROGEN_CONTENT_TERM_ID = "nitrogenContent"
194
- LIGNIN_CONTENT_TERM_ID = "ligninContent"
195
-
196
- CARBON_INPUT_PROPERTY_TERM_IDS = [
197
- CARBON_CONTENT_TERM_ID,
198
- NITROGEN_CONTENT_TERM_ID,
199
- LIGNIN_CONTENT_TERM_ID
200
- ]
201
-
202
- CARBON_SOURCE_TERM_TYPES = [
203
- TermTermType.ORGANICFERTILISER.value,
204
- TermTermType.SOILAMENDMENT.value
205
- ]
206
-
207
- MIN_RUN_IN_PERIOD = 5
208
-
209
- DEFAULT_PARAMS = {
210
- "active_decay_factor": 7.4,
211
- "slow_decay_factor": 0.209,
212
- "passive_decay_factor": 0.00689,
213
- "f_1": 0.378,
214
- "f_2_full_tillage": 0.455,
215
- "f_2_reduced_tillage": 0.477,
216
- "f_2_no_tillage": 0.5,
217
- "f_2_unknown_tillage": 0.368,
218
- "f_3": 0.455,
219
- "f_5": 0.0855,
220
- "f_6": 0.0504,
221
- "f_7": 0.42,
222
- "f_8": 0.45,
223
- "tillage_factor_full_tillage": 3.036,
224
- "tillage_factor_reduced_tillage": 2.075,
225
- "tillage_factor_no_tillage": 1,
226
- "maximum_temperature": 45,
227
- "optimum_temperature": 33.69,
228
- "water_factor_slope": 1.331,
229
- "default_carbon_content": 0.42,
230
- "default_nitrogen_content": 0.0085,
231
- "default_lignin_content": 0.073
232
- }
233
-
234
- VALID_SITE_TYPES_TIER_2 = [
235
- SiteSiteType.CROPLAND.value
236
- ]
237
-
238
- VALID_FUNCTIONAL_UNITS_TIER_2 = [
239
- CycleFunctionalUnit._1_HA.value
240
- ]
241
-
242
- # --- TIER 1 CONSTANTS ---
243
-
244
- CLAY_CONTENT_TERM_ID = "clayContent"
245
- LONG_FALLOW_CROP_TERM_ID = "longFallowCrop"
246
- IMPROVED_PASTURE_TERM_ID = "improvedPasture"
247
- SHORT_BARE_FALLOW_TERM_ID = "shortBareFallow"
248
- ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
249
- INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
250
- ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserUsed"
251
- SOIL_AMENDMENT_USED_TERM_ID = "amendmentIncreasingSoilCarbonUsed"
252
-
253
- CLAY_CONTENT_MAX = 8
254
- SAND_CONTENT_MIN = 70
255
-
256
- EQUILIBRIUM_TRANSITION_PERIOD = 20
257
- """
258
- The number of years required for soil organic carbon to reach equilibrium after
259
- a change in land use, management regime or carbon input regime.
260
- """
261
-
262
- EXCLUDED_ECO_CLIMATE_ZONES_TIER_1 = {
263
- 5, # Polar Moist
264
- 6 # Polar Dry
265
- }
266
-
267
- VALID_SITE_TYPES_TIER_1 = [
268
- SiteSiteType.CROPLAND.value,
269
- SiteSiteType.FOREST.value,
270
- SiteSiteType.OTHER_NATURAL_VEGETATION.value,
271
- SiteSiteType.PERMANENT_PASTURE.value,
272
- ]
273
-
274
- # --- SHARED TIER 1 & TIER 2 FORMAT MEASUREMENT OUTPUT ---
275
-
276
-
277
- def _measurement(year: int, value: float, method_classification: str) -> dict:
278
- """
279
- Build a Hestia `Measurement` node to contain a value calculated by the models.
280
-
281
- Parameters
282
- ----------
283
- year : int
284
- The year that the value is associated with.
285
- value : float
286
- The value calculated by either the Tier 1 or Tier 2 model.
287
- method_classification :str
288
- The method tier used to calculate the value, either `tier 1 model` or `tier 2 model`.
289
-
290
- Returns
291
- -------
292
- dict
293
- A valid Hestia `Measurement` node, see: https://www.hestia.earth/schema/Measurement.
294
- """
295
- measurement = _new_measurement(TERM_ID)
296
- measurement["value"] = [value]
297
- measurement["dates"] = [f"{year}-12-31"]
298
- measurement["depthUpper"] = DEPTH_UPPER
299
- measurement["depthLower"] = DEPTH_LOWER
300
- measurement["methodClassification"] = method_classification
301
- return measurement
302
-
303
-
304
- # --- SHARED TIER 1 & TIER 2 ENUMS ---
305
-
306
- class IpccManagementCategory(Enum):
307
- """
308
- Enum representing IPCC Management Categories for grasslands and annual croplands.
309
-
310
- See [IPCC (2019) Vol. 4, Ch. 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more
311
- information.
312
- """
313
- SEVERELY_DEGRADED = "severely degraded"
314
- IMPROVED_GRASSLAND = "improved grassland"
315
- HIGH_INTENSITY_GRAZING = "high-intensity grazing"
316
- NOMINALLY_MANAGED = "nominally managed"
317
- FULL_TILLAGE = "full tillage"
318
- REDUCED_TILLAGE = "reduced tillage"
319
- NO_TILLAGE = "no tillage"
320
- OTHER = "other"
321
-
322
-
323
- class _InventoryKey(Enum):
324
- """
325
- Enum representing the inner keys of the annual inventory is constructed from site and cycle data.
326
- """
327
- # Tier 1
328
- LU_CATEGORY = 'ipcc land use category'
329
- MG_CATEGORY = 'ipcc management category'
330
- CI_CATEGORY = 'ipcc carbon input category'
331
- SHOULD_RUN_TIER_1 = 'should run tier 1'
332
- # Tier 2
333
- TEMP_MONTHLY = 'temperature monthly'
334
- PRECIP_MONTHLY = 'precipitation monthly'
335
- PET_MONTHLY = 'PET monthly'
336
- IRRIGATED_MONTHLY = 'irrigated monthly'
337
- CARBON_INPUT = 'carbon input'
338
- N_CONTENT = 'nitrogen content'
339
- LIGNIN_CONTENT = 'lignin content'
340
- TILLAGE_CATEGORY = 'ipcc tillage category'
341
- SAND_CONTENT = 'sand content'
342
- IS_PADDY_RICE = 'is paddy rice'
343
- SHOULD_RUN_TIER_2 = 'should run tier 2'
344
-
345
-
346
- REQUIRED_KEYS_TIER_1 = [
347
- _InventoryKey.LU_CATEGORY,
348
- _InventoryKey.MG_CATEGORY,
349
- _InventoryKey.CI_CATEGORY
350
- ]
351
-
352
-
353
- REQUIRED_KEYS_TIER_2 = [
354
- _InventoryKey.TEMP_MONTHLY,
355
- _InventoryKey.PRECIP_MONTHLY,
356
- _InventoryKey.PET_MONTHLY,
357
- _InventoryKey.CARBON_INPUT,
358
- _InventoryKey.N_CONTENT,
359
- _InventoryKey.LIGNIN_CONTENT,
360
- _InventoryKey.TILLAGE_CATEGORY,
361
- _InventoryKey.IS_PADDY_RICE
362
- ]
363
-
364
-
365
- # --- TIER 1 ENUMS ---
366
-
367
-
368
- class IpccSoilCategory(Enum):
369
- """
370
- Enum representing IPCC Soil Categories.
371
-
372
- See [IPCC (2019) Vol 4, Ch. 2 and 3](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more
373
- information.
374
- """
375
- ORGANIC_SOILS = "organic soils"
376
- SANDY_SOILS = "sandy soils"
377
- WETLAND_SOILS = "wetland soils"
378
- VOLCANIC_SOILS = "volcanic soils"
379
- SPODIC_SOILS = "spodic soils"
380
- HIGH_ACTIVITY_CLAY_SOILS = "high-activity clay soils"
381
- LOW_ACTIVITY_CLAY_SOILS = "low-activity clay soils"
382
-
383
-
384
- class IpccLandUseCategory(Enum):
385
- """
386
- Enum representing IPCC Land Use Categories.
387
-
388
- See [IPCC (2019) Vol 4](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
389
- """
390
- GRASSLAND = "grassland"
391
- PERENNIAL_CROPS = "perennial crops"
392
- PADDY_RICE_CULTIVATION = "paddy rice cultivation"
393
- ANNUAL_CROPS_WET = "annual crops (wet)"
394
- ANNUAL_CROPS = "annual crops"
395
- SET_ASIDE = "set aside"
396
- FOREST = "forest"
397
- NATIVE = "native"
398
- OTHER = "other"
399
-
400
-
401
- class IpccCarbonInputCategory(Enum):
402
- """
403
- Enum representing IPCC Carbon Input Categories for improved grasslands and annual croplands.
404
-
405
- See [IPCC (2019) Vol. 4, Ch. 4, 5 and 6](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more
406
- information.
407
- """
408
- GRASSLAND_HIGH = "grassland high"
409
- GRASSLAND_MEDIUM = "grassland medium"
410
- CROPLAND_HIGH_WITH_MANURE = "cropland high (with manure)"
411
- CROPLAND_HIGH_WITHOUT_MANURE = "cropland high (without manure)"
412
- CROPLAND_MEDIUM = "cropland medium"
413
- CROPLAND_LOW = "cropland low"
414
- OTHER = "other"
415
-
416
-
417
- # --- TIER 2 NAMED TUPLES FOR CARBON SOURCES AND MODEL RESULTS ---
418
-
419
-
420
- CarbonSource = NamedTuple(
421
- "CarbonSource",
422
- [
423
- ("mass", float),
424
- ("carbon_content", float),
425
- ("nitrogen_content", float),
426
- ("lignin_content", float),
427
- ]
428
- )
429
- """
430
- A single carbon source (e.g. crop residues or organic amendment).
431
-
432
- Attributes
433
- -----------
434
- mass : float
435
- The dry-matter mass of the carbon source, kg ha-1
436
- carbon_content : float
437
- The carbon content of the carbon source, decimal proportion, kg C (kg d.m.)-1.
438
- nitrogen_content : float
439
- The nitrogen content of the carbon source, decimal_proportion, kg N (kg d.m.)-1.
440
- lignin_content : float
441
- The lignin content of the carbon source, decimal_proportion, kg lignin (kg d.m.)-1.
442
- """
443
-
444
-
445
- TemperatureFactorResult = NamedTuple(
446
- "TemperatureFactorResult",
447
- [
448
- ("timestamps", list[float]),
449
- ("annual_temperature_factors", list[float])
450
- ]
451
- )
452
- """
453
- A named tuple to hold the result of `_run_annual_temperature_factors`.
454
-
455
- Attributes
456
- ----------
457
- timestamps : list[int]
458
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
459
- annual_temperature_factors : list[float]
460
- A list of annual temperature factors for each year in the inventory, dimensionless, between `0` and `1`.
461
- """
462
-
463
-
464
- WaterFactorResult = NamedTuple(
465
- "WaterFactorResult",
466
- [
467
- ("timestamps", list[float]),
468
- ("annual_water_factors", list[float])
469
- ]
470
- )
471
- """
472
- A named tuple to hold the result of `_run_annual_water_factors`.
473
-
474
- Attributes
475
- ----------
476
- timestamps : list[int]
477
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
478
- annual_water_factors : list[float]
479
- A list of annual water factors for each year in the inventory, dimensionless, between `0.31935` and `2.25`.
480
- """
481
-
482
-
483
- Tier2SocResult = NamedTuple(
484
- "Tier2SocResult",
485
- [
486
- ("timestamps", list[float]),
487
- ("active_pool_soc_stocks", list[float]),
488
- ("slow_pool_soc_stocks", list[float]),
489
- ("passive_pool_soc_stocks", list[float]),
490
- ]
491
- )
492
- """
493
- A named tuple to hold the result of `_run_soc_stocks`.
494
-
495
- Attributes
496
- ----------
497
- timestamps : list[int]
498
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
499
- active_pool_soc_stocks : list[float]
500
- The active sub-pool SOC stock for each year in the inventory, kg C ha-1.
501
- slow_pool_soc_stocks : list[float]
502
- The slow sub-pool SOC stock for each year in the inventory, kg C ha-1.
503
- passive_pool_soc_stocks : list[float]
504
- The passive sub-pool SOC stock for each year in the inventory, kg C ha-1.
505
- """
506
-
507
-
508
- # --- TIER 1 NAMED TUPLES FOR STOCK CHANGE FACTORS AND MODEL RESULTS ---
509
-
510
-
511
- StockChangeFactors = NamedTuple("StockChangeFactors", [
512
- ("land_use_factor", float),
513
- ("management_factor", float),
514
- ("carbon_input_factor", float)
515
- ])
516
- """
517
- A named tuple to hold the 3 stock change factors retrieved by the model for each year in the inventory.
518
-
519
- Attributes
520
- ----------
521
- land_use_factor : float
522
- The stock change factor for mineral soil organic C land-use systems or sub-systems for a particular land-use,
523
- dimensionless.
524
- management_factor : float
525
- The stock change factor for mineral soil organic C for management regime, dimensionless.
526
- carbon_input_factor : float
527
- The stock change factor for mineral soil organic C for the input of organic amendments, dimensionless.
528
- """
529
-
530
-
531
- # --- SHARED TIER 1 & TIER 2 MAPPING DICTS ---
532
-
533
-
534
- IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE = {
535
- IpccManagementCategory.FULL_TILLAGE: "Full tillage",
536
- IpccManagementCategory.REDUCED_TILLAGE: "Reduced tillage",
537
- IpccManagementCategory.NO_TILLAGE: "No tillage"
538
- }
539
- """
540
- A dictionary mapping IPCC management categories to corresponding tillage lookup values in the
541
- `"IPCC_TILLAGE_MANAGEMENT_CATEGORY" column`.
542
- """
543
-
544
-
545
- # --- TIER 1 MAPPING DICTS ---
546
-
547
-
548
- IPCC_CATEGORY_TO_ECO_CLIMATE_ZONE_LOOKUP_COLUMN = {
549
- # IpccSoilCategory
550
- IpccSoilCategory.SANDY_SOILS: LOOKUPS["ecoClimateZone"][0],
551
- IpccSoilCategory.WETLAND_SOILS: LOOKUPS["ecoClimateZone"][1],
552
- IpccSoilCategory.VOLCANIC_SOILS: LOOKUPS["ecoClimateZone"][2],
553
- IpccSoilCategory.SPODIC_SOILS: LOOKUPS["ecoClimateZone"][3],
554
- IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: LOOKUPS["ecoClimateZone"][4],
555
- IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: LOOKUPS["ecoClimateZone"][5],
556
- # IpccLandUseCategory
557
- IpccLandUseCategory.GRASSLAND: LOOKUPS["ecoClimateZone"][6],
558
- IpccLandUseCategory.PERENNIAL_CROPS: LOOKUPS["ecoClimateZone"][7],
559
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: LOOKUPS["ecoClimateZone"][8],
560
- IpccLandUseCategory.ANNUAL_CROPS_WET: LOOKUPS["ecoClimateZone"][9],
561
- IpccLandUseCategory.ANNUAL_CROPS: LOOKUPS["ecoClimateZone"][10],
562
- IpccLandUseCategory.SET_ASIDE: LOOKUPS["ecoClimateZone"][11],
563
- # IpccManagementCategory
564
- IpccManagementCategory.SEVERELY_DEGRADED: LOOKUPS["ecoClimateZone"][12],
565
- IpccManagementCategory.IMPROVED_GRASSLAND: LOOKUPS["ecoClimateZone"][13],
566
- IpccManagementCategory.HIGH_INTENSITY_GRAZING: LOOKUPS["ecoClimateZone"][14],
567
- IpccManagementCategory.NOMINALLY_MANAGED: LOOKUPS["ecoClimateZone"][15],
568
- IpccManagementCategory.FULL_TILLAGE: LOOKUPS["ecoClimateZone"][16],
569
- IpccManagementCategory.REDUCED_TILLAGE: LOOKUPS["ecoClimateZone"][17],
570
- IpccManagementCategory.NO_TILLAGE: LOOKUPS["ecoClimateZone"][18],
571
- # IpccCarbonInputCategory
572
- IpccCarbonInputCategory.GRASSLAND_HIGH: LOOKUPS["ecoClimateZone"][19],
573
- IpccCarbonInputCategory.GRASSLAND_MEDIUM: LOOKUPS["ecoClimateZone"][20],
574
- IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE: LOOKUPS["ecoClimateZone"][21],
575
- IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE: LOOKUPS["ecoClimateZone"][22],
576
- IpccCarbonInputCategory.CROPLAND_MEDIUM: LOOKUPS["ecoClimateZone"][23],
577
- IpccCarbonInputCategory.CROPLAND_LOW: LOOKUPS["ecoClimateZone"][24]
578
- }
579
- """
580
- A dictionary mapping IPCC category enums to their corresponding eco-climate zone lookup columns.
581
- """
582
-
583
-
584
- def _get_eco_climate_zone_lookup_column(
585
- ipcc_category: Union[
586
- IpccSoilCategory,
587
- IpccLandUseCategory,
588
- IpccManagementCategory,
589
- IpccCarbonInputCategory
590
- ]
591
- ) -> Optional[str]:
592
- """
593
- Retrieve the corresponding eco-climate zone lookup column for the given IPCC category.
594
-
595
- Parameters
596
- ----------
597
- ipcc_category : IpccSoilCategory | IpccLandUseCategory | IpccManagementCategory | IpccCarbonInputCategory
598
- The IPCC category for which the eco-climate zone lookup column is needed.
599
-
600
- Returns
601
- -------
602
- str | None
603
- The eco-climate zone lookup column associated with the provided
604
- IPCC category, or None if no mapping is found.
605
- """
606
- return IPCC_CATEGORY_TO_ECO_CLIMATE_ZONE_LOOKUP_COLUMN.get(ipcc_category, None)
607
-
123
+ _METHOD_TIERS = [tier_2, tier_1]
608
124
 
609
- IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE = {
610
- IpccSoilCategory.ORGANIC_SOILS: "Organic soils",
611
- IpccSoilCategory.SANDY_SOILS: "Sandy soils",
612
- IpccSoilCategory.WETLAND_SOILS: "Wetland soils",
613
- IpccSoilCategory.VOLCANIC_SOILS: "Volcanic soils",
614
- IpccSoilCategory.SPODIC_SOILS: "Spodic soils",
615
- IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: "High-activity clay soils",
616
- IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: "Low-activity clay soils",
617
- }
618
- """
619
- A dictionary mapping IPCC soil categories to corresponding soil type and USDA soil type lookup values in the
620
- `"IPCC_SOIL_CATEGORY"` column.
621
- """
622
-
623
- SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY = {
624
- SiteSiteType.PERMANENT_PASTURE.value: IpccLandUseCategory.GRASSLAND,
625
- SiteSiteType.FOREST.value: IpccLandUseCategory.FOREST,
626
- SiteSiteType.OTHER_NATURAL_VEGETATION.value: IpccLandUseCategory.NATIVE
627
- }
628
- """
629
- A dictionary mapping site types to corresponding IPCC land use categories.
630
- """
631
-
632
- IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE = {
633
- IpccLandUseCategory.GRASSLAND: "Grassland",
634
- IpccLandUseCategory.PERENNIAL_CROPS: "Perennial crops",
635
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: "Paddy rice cultivation",
636
- IpccLandUseCategory.ANNUAL_CROPS_WET: "Annual crops",
637
- IpccLandUseCategory.ANNUAL_CROPS: "Annual crops",
638
- IpccLandUseCategory.SET_ASIDE: [
639
- "Annual crops", "Paddy rice cultivation", "Perennial crops", "Set aside"
640
- ],
641
- IpccLandUseCategory.FOREST: "Forest",
642
- IpccLandUseCategory.NATIVE: "Native"
643
- }
644
- """
645
- A dictionary mapping IPCC land use categories to corresponding land cover lookup values in the
646
- `"IPCC_LAND_USE_CATEGORY"` column.
647
- """
648
-
649
- IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID = {
650
- IpccManagementCategory.SEVERELY_DEGRADED: "severelyDegradedPasture",
651
- IpccManagementCategory.IMPROVED_GRASSLAND: "improvedPasture",
652
- IpccManagementCategory.HIGH_INTENSITY_GRAZING: "highIntensityGrazingPasture",
653
- IpccManagementCategory.NOMINALLY_MANAGED: "nominallyManagedPasture",
654
- IpccManagementCategory.OTHER: "nativePasture"
655
- }
656
- """
657
- A dictionary mapping IPCC management categories to corresponding grassland management term IDs from the land cover
658
- glossary.
659
- """
660
125
 
661
-
662
- # --- TIER 2 FUNCTIONS: ASSIGN TILLAGE CATEGORY TO CYCLES ---
663
-
664
-
665
- def _check_zero_tillages(practices: list[dict]) -> bool:
666
- """
667
- Checks whether a list of `Practice`s nodes describe 0 total tillages, or not.
668
-
669
- Parameters
670
- ----------
671
- practices : list[dict]
672
- A list of Hestia `Practice` nodes, see: https://www.hestia.earth/schema/Practice.
673
-
674
- Returns
675
- -------
676
- bool
677
- Whether or not 0 tillages counted.
678
- """
679
- practice = find_term_match(practices, NUMBER_OF_TILLAGES_TERM_ID)
680
- nTillages = list_sum(practice.get("value", []))
681
- return nTillages <= 0
682
-
683
-
684
- def _check_cycle_tillage_management_category(
685
- cycle: dict,
686
- key: IpccManagementCategory
687
- ) -> bool:
126
+ def run(site: dict) -> list[dict]:
688
127
  """
689
- Checks whether a Hesita `Cycle` node meets the requirements of a specific tillage `IpccManagementCategory`.
128
+ Run both tiers of the IPCC (2019) SOC model.
690
129
 
691
130
  Parameters
692
131
  ----------
693
- cycle : dict
694
- A Hestia `Cycle` node, see: https://www.hestia.earth/schema/Cycle.
695
- key : IpccManagementCategory
696
- The `IpccManagementCategory` to match.
132
+ site : dict
133
+ A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
697
134
 
698
135
  Returns
699
136
  -------
700
- bool
701
- Whether or not the cycle meets the requirements for the category.
137
+ list[dict]
138
+ A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
702
139
  """
703
- LOOKUP = LOOKUPS["tillage"]
704
- target_lookup_values = IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE.get(key, None)
705
-
706
- practices = cycle.get("practices", [])
707
- tillage_nodes = filter_list_term_type(
708
- practices, [TermTermType.TILLAGE]
709
- )
710
-
711
- return cumulative_nodes_lookup_match(
712
- tillage_nodes,
713
- lookup=LOOKUP,
714
- target_lookup_values=target_lookup_values,
715
- cumulative_threshold=MIN_AREA_THRESHOLD
716
- ) and (
717
- key is not IpccManagementCategory.NO_TILLAGE
718
- or _check_zero_tillages(tillage_nodes)
719
- )
720
-
721
-
722
- TIER_2_TILLAGE_MANAGEMENT_CATEGORY_DECISION_TREE = {
723
- IpccManagementCategory.FULL_TILLAGE: (
724
- lambda cycles, key: any(
725
- _check_cycle_tillage_management_category(cycle, key) for cycle in cycles
726
- )
727
- ),
728
- IpccManagementCategory.REDUCED_TILLAGE: (
729
- lambda cycles, key: any(
730
- _check_cycle_tillage_management_category(cycle, key) for cycle in cycles
731
- )
732
- ),
733
- IpccManagementCategory.NO_TILLAGE: (
734
- lambda cycles, key: any(
735
- _check_cycle_tillage_management_category(cycle, key) for cycle in cycles
736
- )
140
+ should_run, run_data = _should_run(site)
141
+ _log_data(site, should_run, run_data)
142
+ return reduce(
143
+ lambda result, method: result + _run_method(method, **run_data[method]),
144
+ run_data,
145
+ list()
737
146
  )
738
- }
739
147
 
740
148
 
741
- def _assign_tier_2_ipcc_tillage_management_category(
742
- cycles: list[dict],
743
- default: IpccManagementCategory = IpccManagementCategory.OTHER
744
- ) -> IpccManagementCategory:
745
- """
746
- Assigns a tillage `IpccManagementCategory` to a list of Hestia `Cycle`s.
149
+ def _should_run(site: dict) -> tuple[bool, dict[ModuleType, dict]]:
150
+ # List of tuples `(should_run, inventory, kwargs, logs)` for each method tier.
151
+ INNER_KEYS = ("should_run", "inventory", "kwargs", "logs")
152
+ run_data = {
153
+ method: {
154
+ key: value for key, value in zip(INNER_KEYS, method.should_run(site))
155
+ } for method in _METHOD_TIERS
156
+ }
157
+ should_run = any(data["should_run"] for data in run_data.values())
158
+ return should_run, run_data
747
159
 
748
- Parameters
749
- ----------
750
- cycles : list[dict])
751
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
752
160
 
753
- Returns
754
- -------
755
- IpccManagementCategory: The assigned tillage `IpccManagementCategory`.
161
+ def _log_data(site: dict, should_run: bool, run_data: dict[ModuleType, dict]) -> None:
756
162
  """
757
- return next(
758
- (
759
- key for key in TIER_2_TILLAGE_MANAGEMENT_CATEGORY_DECISION_TREE
760
- if TIER_2_TILLAGE_MANAGEMENT_CATEGORY_DECISION_TREE[key](cycles, key)
761
- ),
762
- default
763
- ) if len(cycles) > 0 else default
764
-
765
-
766
- # --- TIER 2 FUNCTIONS: ANNUAL TEMPERATURE FACTOR FROM MONTHLY TEMPERATURE DATA ---
767
-
768
-
769
- def _calc_temperature_factor(
770
- average_temperature: float,
771
- maximum_temperature: float = 45.0,
772
- optimum_temperature: float = 33.69,
773
- ) -> float:
163
+ Format and log the inventory, kwargs and any other requirement data for all tier methodologies of the model.
774
164
  """
775
- Equation 5.0E, part 2. Calculate the temperature effect on decomposition in mineral soils for a single month using
776
- the Steady-State Method.
777
-
778
- If `average_temperature >= maximum_temperature` the function should always return 0.
779
-
780
- Parameters
781
- ----------
782
- average_temperature : float
783
- The average air temperature of a given month, degrees C.
784
- maximum_temperature : float
785
- The maximum air temperature for decomposition, degrees C, default value: `45.0`.
786
- optimum_temperature : float
787
- The optimum air temperature for decomposition, degrees C, default value: `33.69`.
165
+ inventory = reduce(merge, [data["inventory"] for data in run_data.values()], dict())
166
+ kwargs = reduce(merge, [data["kwargs"] for data in run_data.values()], dict())
167
+ logs = reduce(merge, [data["logs"] for data in run_data.values()], dict())
788
168
 
789
- Returns
790
- -------
791
- float
792
- The air temperature effect on decomposition for a given month, dimensionless, between `0` and `1`.
793
- """
794
- prelim = (maximum_temperature - average_temperature) / (
795
- maximum_temperature - optimum_temperature
796
- )
797
- return 0 if average_temperature >= maximum_temperature else (
798
- pow(prelim, 0.2) * exp((0.2 / 2.63) * (1 - pow(prelim, 2.63)))
169
+ logRequirements(
170
+ site,
171
+ model=MODEL,
172
+ term=TERM_ID,
173
+ **logs,
174
+ **kwargs,
175
+ inventory=_format_inventory(inventory)
799
176
  )
177
+ logShouldRun(site, MODEL, TERM_ID, should_run)
800
178
 
801
179
 
802
- def _calc_annual_temperature_factor(
803
- average_temperature_monthly: list[float],
804
- maximum_temperature: float = 45.0,
805
- optimum_temperature: float = 33.69,
806
- ) -> Union[float, None]:
807
- """
808
- Equation 5.0E, part 1. Calculate the average annual temperature effect on decomposition in mineral soils using the
809
- Steady-State Method.
810
-
811
- Parameters
812
- ----------
813
- average_temperature_monthly : list[float]
814
- A list of monthly average air temperatures in degrees C, must have a length of 12.
815
-
816
- Returns
817
- -------
818
- float | None
819
- Average annual temperature factor, dimensionless, between `0` and `1`, or `None` if the input list is empty.
820
- """
821
- return mean(
822
- list(
823
- _calc_temperature_factor(t, maximum_temperature, optimum_temperature)
824
- for t in average_temperature_monthly
825
- )
826
- ) if average_temperature_monthly else None
827
-
828
-
829
- # --- TIER 2 FUNCTIONS: ANNUAL WATER FACTOR FROM MONTHLY PRECIPITATION, PET AND IRRIGATION DATA ---
830
-
831
-
832
- def _calc_water_factor(
833
- precipitation: float,
834
- pet: float,
835
- is_irrigated: bool = False,
836
- water_factor_slope: float = 1.331,
837
- ) -> float:
180
+ def _format_inventory(inventory: dict) -> str:
838
181
  """
839
- Equation 5.0F, part 2. Calculate the water effect on decomposition in mineral soils for a single month using the
840
- Steady-State Method.
841
-
842
- If `is_irrigated == True` the function should always return `0.775`.
843
-
844
- Parameters
845
- ----------
846
- precipitation : float
847
- The sum total precipitation of a given month, mm.
848
- pet : float
849
- The sum total potential evapotranspiration in a given month, mm.
850
- is_irrigated : bool
851
- Whether or not irrigation has been used in a given month.
852
- water_factor_slope : float
853
- The slope for mappet term to estimate water factor, dimensionless, default value: `1.331`.
854
-
855
- Returns
856
- -------
857
- float
858
- The water effect on decomposition for a given month, dimensionless, between `0.2129` and `1.5`.
182
+ Format the inventory as a table.
859
183
  """
860
- mappet = min(1.25, precipitation / pet) if pet else 1.25
861
- return 0.775 if is_irrigated else 0.2129 + (water_factor_slope * (mappet)) - (0.2413 * pow(mappet, 2))
184
+ inventory_keys = _get_unique_inventory_keys(inventory)
185
+ return log_as_table(
186
+ {
187
+ "year": year,
188
+ **{
189
+ key.value: _INVENTORY_KEY_TO_FORMAT_FUNC[key](group.get(key))
190
+ for key in inventory_keys
191
+ }
192
+ } for year, group in inventory.items()
193
+ ) if inventory else "None"
862
194
 
863
195
 
864
- def _calc_annual_water_factor(
865
- precipitation_monthly: list[float],
866
- pet_monthly: list[float],
867
- is_irrigated_monthly: Union[list[bool], None] = None,
868
- water_factor_slope: float = 1.331,
869
- ) -> Union[float, None]:
196
+ def _get_unique_inventory_keys(inventory: dict) -> list:
870
197
  """
871
- Equation 5.0F, part 1. Calculate the average annual water effect on decomposition in mineral soils using the
872
- Steady-State Method multiplied by a coefficient of `1.5`.
873
-
874
- Parameters
875
- ----------
876
- precipitation_monthly : list[float]
877
- A list of monthly sum total precipitation values in mm, must have a length of 12.
878
- pet_monthly : list[float])
879
- A list of monthly sum total potential evapotranspiration values in mm, must have a length of 12.
880
- is_irrigated_monthly : list[boolean] | None)
881
- A list of true/false values that describe whether irrigation has been used in each calendar month, must have a
882
- length of 12. If `None` is provided, a list of 12 `False` values is used.
883
- water_factor_slope : float
884
- The slope for mappet term to estimate water factor, dimensionless, default value: `1.331`.
885
-
886
- Returns
887
- -------
888
- float | None
889
- Average annual water factor multiplied by `1.5`, dimensionless, between `0.31935` and `2.25`,
890
- or `None` if any of the input lists are empty.
198
+ Return a list of unique inventory keys in a fixed order.
891
199
  """
892
- is_irrigated_monthly = (
893
- [False] * 12 if is_irrigated_monthly is None else is_irrigated_monthly
200
+ unique_keys = reduce(
201
+ lambda result, keys: result | set(keys),
202
+ (
203
+ (key for key in group.keys() if key in _INVENTORY_KEY_TO_FORMAT_FUNC)
204
+ for group in inventory.values()
205
+ ),
206
+ set()
894
207
  )
895
- zipped = zip(precipitation_monthly, pet_monthly, is_irrigated_monthly)
896
- return 1.5 * mean(list(
897
- _calc_water_factor(precipitation, pet, is_irrigated, water_factor_slope)
898
- for precipitation, pet, is_irrigated in zipped
899
- )) if all([precipitation_monthly, pet_monthly]) else None
900
-
901
-
902
- # --- TIER 2 FUNCTIONS: ANNUAL TOTAL ORGANIC C INPUT TO SOIL, N CONTENT AND LIGNIN CONTENT FROM CARBON SOURCES ---
208
+ key_order = {key: i for i, key in enumerate(_INVENTORY_KEY_TO_FORMAT_FUNC.keys())}
209
+ return sorted(unique_keys, key=lambda key_: key_order[key_])
903
210
 
904
211
 
905
- def _calc_total_organic_carbon_input(
906
- carbon_sources: list[CarbonSource], default_carbon_content=0.42
907
- ) -> float:
908
- """
909
- Equation 5.0H part 1. Calculate the total organic carbon to a site from all carbon sources (above-ground and
910
- below-ground crop residues, organic amendments, etc.).
911
-
912
- Parameters
913
- ----------
914
- carbon_sources : list[CarbonSource])
915
- A list of carbon sources as named tuples with the format
916
- `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`.
917
- default_carbon_content : float
918
- The default carbon content of a carbon source, decimal proportion, kg C (kg d.m.)-1.
919
-
920
- Returns
921
- -------
922
- float
923
- The total mass of organic carbon inputted into the site, kg C ha-1.
924
- """
925
- return sum(c.mass * (c.carbon_content if c.carbon_content else default_carbon_content) for c in carbon_sources)
926
-
927
-
928
- def _calc_average_nitrogen_content_of_organic_carbon_sources(
929
- carbon_sources: list[CarbonSource], default_nitrogen_content=0.0085
930
- ) -> float:
931
- """
932
- Calculate the average nitrogen content of the carbon inputs through a weighted mean.
933
-
934
- Parameters
935
- ----------
936
- carbon_sources : list[CarbonSource]
937
- A list of carbon sources as named tuples with the format
938
- `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`
939
- default_nitrogen_content : float
940
- The default nitrogen content of a carbon source, decimal proportion, kg N (kg d.m.)-1.
941
-
942
- Returns
943
- -------
944
- float
945
- The average nitrogen content of the carbon sources, decimal_proportion, kg N (kg d.m.)-1.
946
- """
947
- total_weight = sum(c.mass for c in carbon_sources)
948
- weighted_values = [
949
- c.mass * (c.nitrogen_content if c.nitrogen_content else default_nitrogen_content) for c in carbon_sources
950
- ]
951
- should_run = total_weight > 0
952
- return sum(weighted_values) / total_weight if should_run else 0
953
-
954
-
955
- def _calc_average_lignin_content_of_organic_carbon_sources(
956
- carbon_sources: list[dict[str, float]], default_lignin_content=0.073
957
- ) -> float:
958
- """
959
- Calculate the average lignin content of the carbon inputs through a weighted mean.
960
-
961
- Parameters
962
- ----------
963
- carbon_sources : list[CarbonSource]
964
- A list of carbon sources as named tuples with the format
965
- `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`
966
- default_lignin_content : float
967
- The default lignin content of a carbon source, decimal proportion, kg lignin (kg d.m.)-1.
968
-
969
- Returns
970
- -------
971
- float
972
- The average lignin content of the carbon sources, decimal_proportion, kg lignin (kg d.m.)-1.
973
- """
974
- total_weight = sum(c.mass for c in carbon_sources)
975
- weighted_values = [
976
- c.mass * (c.lignin_content if c.lignin_content else default_lignin_content) for c in carbon_sources
977
- ]
978
- should_run = total_weight > 0
979
- return sum(weighted_values) / total_weight if should_run else 0
980
-
212
+ _INVENTORY_KEY_TO_FORMAT_FUNC = {
213
+ tier_2._InventoryKey.SHOULD_RUN: format_bool,
214
+ tier_2._InventoryKey.TEMP_MONTHLY: format_number_list,
215
+ tier_2._InventoryKey.PRECIP_MONTHLY: format_number_list,
216
+ tier_2._InventoryKey.PET_MONTHLY: format_number_list,
217
+ tier_2._InventoryKey.IRRIGATED_MONTHLY: format_bool_list,
218
+ tier_2._InventoryKey.SAND_CONTENT: format_number,
219
+ tier_2._InventoryKey.CARBON_INPUT: format_number,
220
+ tier_2._InventoryKey.N_CONTENT: format_number,
221
+ tier_2._InventoryKey.LIGNIN_CONTENT: format_number,
222
+ tier_2._InventoryKey.TILLAGE_CATEGORY: format_enum,
223
+ tier_2._InventoryKey.IS_PADDY_RICE: format_bool,
224
+ tier_1._InventoryKey.SHOULD_RUN: format_bool,
225
+ tier_1._InventoryKey.LU_CATEGORY: format_enum,
226
+ tier_1._InventoryKey.MG_CATEGORY: format_enum,
227
+ tier_1._InventoryKey.CI_CATEGORY: format_enum,
228
+ }
229
+ """
230
+ Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
231
+ the `dict` keys.
232
+ """
981
233
 
982
- # --- TIER 2 FUNCTIONS: ACTIVE SUB-POOL SOC STOCK ---
983
234
 
984
-
985
- def _calc_beta(
986
- carbon_input: float,
987
- lignin_content: float = 0.073,
988
- nitrogen_content: float = 0.0083,
989
- ) -> float:
990
- """
991
- Equation 5.0G, part 2. Calculate the C input to the metabolic dead organic matter C component, kg C ha-1.
992
-
993
- See table 5.5b for default values for lignin content and nitrogen content.
994
-
995
- Parameters
996
- ----------
997
- carbon_input : float
998
- Total carbon input to the soil during an inventory year, kg C ha-1.
999
- lignin_content : float
1000
- The average lignin content of carbon input sources, decimal proportion, default value: `0.073`.
1001
- nitrogen_content : float
1002
- The average nitrogen content of carbon sources, decimal proportion, default value: `0.0083`.
1003
-
1004
- Returns
1005
- -------
1006
- float
1007
- The C input to the metabolic dead organic matter C component, kg C ha-1.
1008
- """
1009
- return carbon_input * (0.85 - 0.018 * (lignin_content / nitrogen_content))
1010
-
1011
-
1012
- def _get_f_2(
1013
- tillage_management_category: IpccManagementCategory = IpccManagementCategory.OTHER,
1014
- f_2_full_tillage: float = 0.455,
1015
- f_2_reduced_tillage: float = 0.477,
1016
- f_2_no_tillage: float = 0.5,
1017
- f_2_unknown_tillage: float = 0.368,
1018
- ) -> float:
1019
- """
1020
- Get the value of `f_2` (the stabilisation efficiencies for structural decay products entering the active pool)
1021
- based on the tillage `IpccManagementCategory`.
1022
-
1023
- If tillage regime is unknown, `IpccManagementCategory.OTHER` should be assumed.
1024
-
1025
- Parameters
1026
- ----------
1027
- tillage_management_category : (IpccManagementCategory)
1028
- The tillage category of the inventory year, default value: `IpccManagementCategory.OTHER`.
1029
- f_2_full_tillage : float
1030
- The stabilisation efficiencies for structural decay products entering the active pool under full tillage,
1031
- decimal proportion, default value: `0.455`.
1032
- f_2_reduced_tillage : float
1033
- The stabilisation efficiencies for structural decay products entering the active pool under reduced tillage,
1034
- decimal proportion, default value: `0.477`.
1035
- f_2_no_tillage : float
1036
- The stabilisation efficiencies for structural decay products entering the active pool under no tillage,
1037
- decimal proportion, default value: `0.5`.
1038
- f_2_unknown_tillage : float
1039
- The stabilisation efficiencies for structural decay products entering the active pool if tillage is not known,
1040
- decimal proportion, default value: `0.368`.
1041
-
1042
- Returns
1043
- -------
1044
- float: The stabilisation efficiencies for structural decay products entering the active pool,
1045
- decimal proportion.
1046
- """
1047
- ipcc_tillage_management_category_to_f_2s = {
1048
- IpccManagementCategory.FULL_TILLAGE: f_2_full_tillage,
1049
- IpccManagementCategory.REDUCED_TILLAGE: f_2_reduced_tillage,
1050
- IpccManagementCategory.NO_TILLAGE: f_2_no_tillage,
1051
- IpccManagementCategory.OTHER: f_2_unknown_tillage
1052
- }
1053
- default = f_2_unknown_tillage
1054
-
1055
- return ipcc_tillage_management_category_to_f_2s.get(tillage_management_category, default)
1056
-
1057
-
1058
- def _calc_f_4(sand_content: float = 0.33, f_5: float = 0.0855) -> float:
1059
- """
1060
- Equation 5.0C, part 4. Calculate the value of the stabilisation efficiencies for active pool decay products
1061
- entering the slow pool based on the sand content of the soil.
1062
-
1063
- Parameters
1064
- ----------
1065
- sand_content : float)
1066
- The sand content of the soil, decimal proportion, default value: `0.33`.
1067
- f_5 : float
1068
- The stabilisation efficiencies for active pool decay products entering the passive pool, decimal_proportion,
1069
- default value: `0.0855`.
1070
-
1071
- Returns
1072
- -------
1073
- float
1074
- The stabilisation efficiencies for active pool decay products entering the slow pool, decimal proportion.
1075
- """
1076
- return 1 - f_5 - (0.17 + 0.68 * sand_content)
1077
-
1078
-
1079
- def _calc_alpha(
1080
- carbon_input: float,
1081
- f_2: float,
1082
- f_4: float,
1083
- lignin_content: float = 0.073,
1084
- nitrogen_content: float = 0.0083,
1085
- f_1: float = 0.378,
1086
- f_3: float = 0.455,
1087
- f_5: float = 0.0855,
1088
- f_6: float = 0.0504,
1089
- f_7: float = 0.42,
1090
- f_8: float = 0.45,
1091
- ) -> float:
1092
- """
1093
- Equation 5.0G, part 1. Calculate the C input to the active soil carbon sub-pool, kg C ha-1.
1094
-
1095
- See table 5.5b for default values for lignin content and nitrogen content.
1096
-
1097
- Parameters
1098
- ----------
1099
- carbon_input : float
1100
- Total carbon input to the soil during an inventory year, kg C ha-1.
1101
- f_2 : float
1102
- The stabilisation efficiencies for structural decay products entering the active pool, decimal proportion.
1103
- f_4 : float
1104
- The stabilisation efficiencies for active pool decay products entering the slow pool, decimal proportion.
1105
- lignin_content : float
1106
- The average lignin content of carbon input sources, decimal proportion, default value: `0.073`.
1107
- nitrogen_content : float
1108
- The average nitrogen content of carbon input sources, decimal proportion, default value: `0.0083`.
1109
- sand_content : float
1110
- The sand content of the soil, decimal proportion, default value: `0.33`.
1111
- f_1 : float
1112
- The stabilisation efficiencies for metabolic decay products entering the active pool, decimal proportion,
1113
- default value: `0.378`.
1114
- f_3 : float
1115
- The stabilisation efficiencies for structural decay products entering the slow pool, decimal proportion,
1116
- default value: `0.455`.
1117
- f_5 : float
1118
- The stabilisation efficiencies for active pool decay products entering the passive pool, decimal proportion,
1119
- default value: `0.0855`.
1120
- f_6 : float
1121
- The stabilisation efficiencies for slow pool decay products entering the passive pool, decimal proportion,
1122
- default value: `0.0504`.
1123
- f_7 : float
1124
- The stabilisation efficiencies for slow pool decay products entering the active pool, decimal proportion,
1125
- default value: `0.42`.
1126
- f_8 : float
1127
- The stabilisation efficiencies for passive pool decay products entering the active pool, decimal proportion,
1128
- default value: `0.45`.
1129
-
1130
- Returns
1131
- -------
1132
- float
1133
- The C input to the active soil carbon sub-pool, kg C ha-1.
1134
- """
1135
- beta = _calc_beta(
1136
- carbon_input, lignin_content=lignin_content, nitrogen_content=nitrogen_content
1137
- )
1138
-
1139
- x = beta * f_1
1140
- y = (carbon_input * (1 - lignin_content) - beta) * f_2
1141
- z = (carbon_input * lignin_content) * f_3 * (f_7 + (f_6 * f_8))
1142
- d = 1 - (f_4 * f_7) - (f_5 * f_8) - (f_4 * f_6 * f_8)
1143
- return (x + y + z) / d
1144
-
1145
-
1146
- def _get_tillage_factor(
1147
- tillage_management_category: IpccManagementCategory = IpccManagementCategory.FULL_TILLAGE,
1148
- tillage_factor_full_tillage: float = 3.036,
1149
- tillage_factor_reduced_tillage: float = 2.075,
1150
- tillage_factor_no_tillage: float = 1,
1151
- ) -> float:
1152
- """
1153
- Calculate the tillage disturbance modifier on decay rate for active and slow sub-pools based on the tillage
1154
- `IpccManagementCategory`.
1155
-
1156
- If tillage regime is unknown, `FULL_TILLAGE` should be assumed.
1157
-
1158
- Parameters
1159
- ----------
1160
- tillage_factor_full_tillage : float)
1161
- The tillage disturbance modifier for decay rates under full tillage, dimensionless, default value: `3.036`.
1162
- tillage_factor_reduced_tillage : float
1163
- Tillage disturbance modifier for decay rates under reduced tillage, dimensionless, default value: `2.075`.
1164
- tillage_factor_no_tillage : float
1165
- Tillage disturbance modifier for decay rates under no tillage, dimensionless, default value: `1`.
1166
-
1167
- Returns
1168
- -------
1169
- float
1170
- The tillage disturbance modifier on decay rate for active and slow sub-pools, dimensionless.
1171
- """
1172
- ipcc_tillage_management_category_to_tillage_factors = {
1173
- IpccManagementCategory.FULL_TILLAGE: tillage_factor_full_tillage,
1174
- IpccManagementCategory.REDUCED_TILLAGE: tillage_factor_reduced_tillage,
1175
- IpccManagementCategory.NO_TILLAGE: tillage_factor_no_tillage,
1176
- }
1177
- default = tillage_factor_full_tillage
1178
-
1179
- return ipcc_tillage_management_category_to_tillage_factors.get(
1180
- tillage_management_category, default
1181
- )
1182
-
1183
-
1184
- def _calc_active_pool_decay_rate(
1185
- annual_temperature_factor: float,
1186
- annual_water_factor: float,
1187
- tillage_factor: float,
1188
- sand_content: float = 0.33,
1189
- active_decay_factor: float = 7.4,
1190
- ) -> float:
1191
- """
1192
- Equation 5.0B, part 3. Calculate the decay rate for the active SOC sub-pool given conditions in an inventory year.
1193
-
1194
- Parameters
1195
- ----------
1196
- annual_temperature_factor : float
1197
- Average annual temperature factor, dimensionless, between `0` and `1`.
1198
- annual_water_factor : float
1199
- Average annual water factor, dimensionless, between `0.31935` and `2.25`.
1200
- tillage_factor : float
1201
- The tillage disturbance modifier on decay rate for active and slow sub-pools, dimensionless.
1202
- sand_content : float
1203
- sand_content (float): The sand content of the soil, decimal proportion, default value: `0.33`.
1204
- active_decay_factor : float
1205
- decay rate constant under optimal conditions for decomposition of the active SOC subpool, year-1, default value:
1206
- `7.4`.
1207
-
1208
- Returns
1209
- -------
1210
- float
1211
- The decay rate for active SOC sub-pool, year-1.
1212
- """
1213
- sand_factor = 0.25 + (0.75 * sand_content)
1214
- return (
1215
- annual_temperature_factor
1216
- * annual_water_factor
1217
- * tillage_factor
1218
- * sand_factor
1219
- * active_decay_factor
1220
- )
1221
-
1222
-
1223
- def _calc_active_pool_steady_state(
1224
- alpha: float, active_pool_decay_rate: float
1225
- ) -> float:
1226
- """
1227
- Equation 5.0B part 2. Calculate the steady state active sub-pool SOC stock given conditions in an inventory year.
1228
-
1229
- Parameters
1230
- ----------
1231
- alpha : float
1232
- The C input to the active soil carbon sub-pool, kg C ha-1.
1233
- active_pool_decay_rate : float
1234
- Decay rate for active SOC sub-pool, year-1.
1235
-
1236
- Returns
1237
- -------
1238
- float
1239
- The steady state active sub-pool SOC stock given conditions in year y, kg C ha-1
1240
- """
1241
- return alpha / active_pool_decay_rate
1242
-
1243
-
1244
- # --- TIER 2 FUNCTIONS: SLOW SUB-POOL SOC STOCK ---
1245
-
1246
-
1247
- def _calc_slow_pool_decay_rate(
1248
- annual_temperature_factor: float,
1249
- annual_water_factor: float,
1250
- tillage_factor: float,
1251
- slow_decay_factor: float = 0.209,
1252
- ) -> float:
1253
- """
1254
- Equation 5.0C, part 3. Calculate the decay rate for the slow SOC sub-pool given conditions in an inventory year.
1255
-
1256
- Parameters
1257
- ----------
1258
- annual_temperature_factor : float
1259
- Average annual temperature factor, dimensionless, between `0` and `1`.
1260
- annual_water_factor : float
1261
- Average annual water factor, dimensionless, between `0.31935` and `2.25`.
1262
- tillage_factor : float
1263
- The tillage disturbance modifier on decay rate for active and slow sub-pools, dimensionless.
1264
- slow_decay_factor : float)
1265
- The decay rate constant under optimal conditions for decomposition of the slow SOC subpool, year-1,
1266
- default value: `0.209`.
1267
-
1268
- Returns
1269
- -------
1270
- float
1271
- The decay rate for slow SOC sub-pool, year-1.
1272
- """
1273
- return (
1274
- annual_temperature_factor
1275
- * annual_water_factor
1276
- * tillage_factor
1277
- * slow_decay_factor
1278
- )
1279
-
1280
-
1281
- def _calc_slow_pool_steady_state(
1282
- carbon_input: float,
1283
- f_4: float,
1284
- active_pool_steady_state: float,
1285
- active_pool_decay_rate: float,
1286
- slow_pool_decay_rate: float,
1287
- lignin_content: float = 0.073,
1288
- f_3: float = 0.455,
1289
- ) -> float:
1290
- """
1291
- Equation 5.0C, part 2. Calculate the steady state slow sub-pool SOC stock given conditions in an inventory year.
1292
-
1293
- Parameters
1294
- ----------
1295
- carbon_input : float
1296
- Total carbon input to the soil during an inventory year, kg C ha-1.
1297
- f_4 : float
1298
- The stabilisation efficiencies for active pool decay products entering the slow pool, decimal proportion.
1299
- active_pool_steady_state : float
1300
- The steady state active sub-pool SOC stock given conditions in year y, kg C ha-1
1301
- active_pool_decay_rate : float
1302
- Decay rate for active SOC sub-pool, year-1.
1303
- slow_pool_decay_rate : float
1304
- Decay rate for slow SOC sub-pool, year-1.
1305
- lignin_content : float
1306
- The average lignin content of carbon input sources, decimal proportion, default value: `0.073`.
1307
- f_3 : float
1308
- The stabilisation efficiencies for structural decay products entering the slow pool, decimal proportion,
1309
- default value: `0.455`.
1310
-
1311
- Returns
1312
- -------
1313
- float
1314
- The steady state slow sub-pool SOC stock given conditions in year y, kg C ha-1
1315
- """
1316
- x = carbon_input * lignin_content * f_3
1317
- y = active_pool_steady_state * active_pool_decay_rate * f_4
1318
- return (x + y) / slow_pool_decay_rate
1319
-
1320
-
1321
- # --- TIER 2 FUNCTIONS: PASSIVE SUB-POOL SOC STOCK ---
1322
-
1323
-
1324
- def _calc_passive_pool_decay_rate(
1325
- annual_temperature_factor: float,
1326
- annual_water_factor: float,
1327
- passive_decay_factor: float = 0.00689,
1328
- ) -> float:
1329
- """
1330
- Equation 5.0D, part 3. Calculate the decay rate for the passive SOC sub-pool given conditions in an inventory year.
1331
-
1332
- Parameters
1333
- ----------
1334
- annual_temperature_factor : float
1335
- Average annual temperature factor, dimensionless, between `0` and `1`.
1336
- annual_water_factor : float
1337
- Average annual water factor, dimensionless, between `0.31935` and `2.25`.
1338
- passive_decay_factor : float
1339
- decay rate constant under optimal conditions for decomposition of the passive SOC subpool, year-1,
1340
- default value: `0.00689`.
1341
-
1342
- Returns
1343
- -------
1344
- float
1345
- The decay rate for passive SOC sub-pool, year-1.
1346
- """
1347
- return annual_temperature_factor * annual_water_factor * passive_decay_factor
1348
-
1349
-
1350
- def _calc_passive_pool_steady_state(
1351
- active_pool_steady_state: float,
1352
- slow_pool_steady_state: float,
1353
- active_pool_decay_rate: float,
1354
- slow_pool_decay_rate: float,
1355
- passive_pool_decay_rate: float,
1356
- f_5: float = 0.0855,
1357
- f_6: float = 0.0504,
1358
- ) -> float:
1359
- """
1360
- Equation 5.0D, part 2. Calculate the steady state passive sub-pool SOC stock given conditions in an inventory year.
1361
-
1362
- Parameters
1363
- ----------
1364
- active_pool_steady_state : float
1365
- The steady state active sub-pool SOC stock given conditions in year y, kg C ha-1.
1366
- slow_pool_steady_state : float
1367
- The steady state slow sub-pool SOC stock given conditions in year y, kg C ha-1.
1368
- active_pool_decay_rate : float
1369
- Decay rate for active SOC sub-pool, year-1.
1370
- slow_pool_decay_rate : float
1371
- Decay rate for slow SOC sub-pool, year-1.
1372
- passive_pool_decay_rate : float
1373
- Decay rate for passive SOC sub-pool, year-1.
1374
- f_5 : float
1375
- The stabilisation efficiencies for active pool decay products entering the passive pool, decimal proportion,
1376
- default value: `0.0855`.
1377
- f_6 : float
1378
- The stabilisation efficiencies for slow pool decay products entering the passive pool, decimal proportion,
1379
- default value: `0.0504`.
1380
-
1381
- Returns
1382
- -------
1383
- float
1384
- The steady state passive sub-pool SOC stock given conditions in year y, kg C ha-1.
1385
- """
1386
- x = active_pool_steady_state * active_pool_decay_rate * f_5
1387
- y = slow_pool_steady_state * slow_pool_decay_rate * f_6
1388
- return (x + y) / passive_pool_decay_rate
1389
-
1390
-
1391
- # --- TIER 2 FUNCTIONS: GENERIC SUB-POOL SOC STOCK ---
1392
-
1393
-
1394
- def _calc_sub_pool_soc_stock(
1395
- sub_pool_steady_state: (float),
1396
- previous_sub_pool_soc_stock: (float),
1397
- sub_pool_decay_rate: (float),
1398
- timestep: int = 1,
1399
- ) -> float:
1400
- """
1401
- Generalised from equations 5.0B, 5.0C and 5.0D, part 1. Calculate the sub-pool SOC stock in year y, kg C ha-1.
1402
-
1403
- If `sub_pool_decay_rate > 1` then set its value to `1` for this calculation.
1404
-
1405
- Parameters
1406
- ----------
1407
- sub_pool_steady_state : float
1408
- The steady state sub-pool SOC stock given conditions in year y, kg C ha-1.
1409
- previous_sub_pool_soc_stock : float
1410
- The sub-pool SOC stock in year y-timestep (by default one year ago), kg C ha-1.
1411
- sub_pool_decay_rate : float
1412
- Decay rate for active SOC sub-pool, year-1.
1413
- timestep : int
1414
- The number of years between current and previous inventory year.
1415
-
1416
- Returns
1417
- -------
1418
- float
1419
- The sub-pool SOC stock in year y, kg C ha-1.
1420
- """
1421
- sub_pool_decay_rate = min(1, sub_pool_decay_rate)
1422
- return (
1423
- previous_sub_pool_soc_stock
1424
- + (sub_pool_steady_state - previous_sub_pool_soc_stock)
1425
- * timestep
1426
- * sub_pool_decay_rate
1427
- )
1428
-
1429
-
1430
- # --- TIER 2 FUNCTIONS: SOC STOCK CHANGE ---
1431
-
1432
-
1433
- def _calc_tier_2_soc_stock(
1434
- active_pool_soc_stock: float,
1435
- slow_pool_soc_stock: float,
1436
- passive_pool_soc_stock: float,
1437
- ) -> float:
1438
- """
1439
- Equation 5.0A, part 3. Calculate the total SOC stock for a site by summing its active, slow and passive SOC stock
1440
- sub-pools. This is the value we need for our `organicCarbonPerHa` measurement.
1441
-
1442
- Parameters
1443
- ----------
1444
- actve_pool_soc_stock : float
1445
- The active sub-pool SOC stock in year y, kg C ha-1.
1446
- slow_pool_soc_stock : float
1447
- The slow sub-pool SOC stock in year y, kg C ha-1.
1448
- passive_pool_soc_stock : float
1449
- The passive sub-pool SOC stock in year y, kg C ha-1.
1450
-
1451
- Returns
1452
- -------
1453
- float
1454
- The SOC stock of a site in year y, kg C ha-1.
1455
- """
1456
- return active_pool_soc_stock + slow_pool_soc_stock + passive_pool_soc_stock
1457
-
1458
-
1459
- # --- TIER 2 SUB-MODEL: RUN ACTIVE, SLOW AND PASSIVE SOC STOCKS ---
1460
-
1461
-
1462
- def timeseries_to_inventory(timeseries_data: list[float], run_in_period: int):
1463
- """
1464
- Convert annual data to inventory data by averaging the values for the run-in period.
1465
-
1466
- Parameters
1467
- ----------
1468
- timeseries_data : list[float]
1469
- The timeseries data to be reformatted.
1470
- run_in_period : int
1471
- The length of the run-in in years.
1472
-
1473
- Returns
1474
- -------
1475
- list[float]
1476
- The inventory formatted data, where value 0 is the average of the run-in values.
1477
- """
1478
- return [mean(timeseries_data[0:run_in_period])] + timeseries_data[run_in_period:]
1479
-
1480
-
1481
- def _run_soc_stocks(
1482
- timestamps: list[int],
1483
- annual_temperature_factors: list[float],
1484
- annual_water_factors: list[float],
1485
- annual_organic_carbon_inputs: list[float],
1486
- annual_n_contents: list[float],
1487
- annual_lignin_contents: list[float],
1488
- annual_tillage_categories: Union[list[IpccManagementCategory], None] = None,
1489
- sand_content: float = 0.33,
1490
- run_in_period: int = 5,
1491
- params: Union[dict[str, float], None] = None,
1492
- ) -> Tier2SocResult:
1493
- """
1494
- Run the IPCC Tier 2 SOC model with precomputed `annual_temperature_factors`, `annual_water_factors`,
1495
- `annual_organic_carbon_inputs`, `annual_n_contents`, `annual_lignin_contents`.
1496
-
1497
- Parameters
1498
- ----------
1499
- timestamps : list[int]
1500
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
1501
- annual_temperature_factors : list[float]
1502
- A list of temperature factors for each year in the inventory, dimensionless (see Equation 5.0E).
1503
- annual_water_factors : list[float]
1504
- A list of water factors for each year in the inventory, dimensionless (see Equation 5.0F).
1505
- annual_organic_carbon_inputs : list[float]
1506
- A list of organic carbon inputs to the soil for each year in the inventory, kg C ha-1 year-1 (see Equation
1507
- 5.0H).
1508
- annual_n_contents : list[float]
1509
- A list of the average nitrogen contents of the organic carbon sources for each year in the inventory, decimal
1510
- proportion.
1511
- annual_lignin_contents : list[float]
1512
- A list of the average lignin contents of the organic carbon sources for each year in the inventory, decimal
1513
- proportion.
1514
- annual_tillage_categories : list[IpccManagementCategory] | None
1515
- A list of the site"s `IpccManagementCategory`s for each year in the inventory.
1516
- sand_content : float
1517
- The sand content of the site, decimal proportion, default value: `0.33`.
1518
- run_in_period : int
1519
- The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
1520
- params : dict[str: float] | None
1521
- Overrides for the model parameters. If `None` only default parameters will be used.
1522
-
1523
- Returns
1524
- -------
1525
- Tier2SocResult
1526
- Returns an annual inventory of organicCarbonPerHa data in the format
1527
- `(timestamps: list[int], organicCarbonPerHa_values: list[float], active_pool_soc_stocks: list[float],
1528
- slow_pool_soc_stocks: list[float], passive_pool_soc_stocks: list[float])`
1529
- """
1530
-
1531
- # --- MERGE ANY USER-SET PARAMETERS WITH THE IPCC DEFAULTS ---
1532
-
1533
- params = DEFAULT_PARAMS | (params or {})
1534
-
1535
- # --- GET F4 ---
1536
-
1537
- f_4 = _calc_f_4(sand_content, f_5=params.get("f_5"))
1538
-
1539
- # --- GET ANNUAL DATA ---
1540
-
1541
- annual_f_2s = [
1542
- _get_f_2(
1543
- till,
1544
- f_2_full_tillage=params.get("f_2_full_tillage"),
1545
- f_2_reduced_tillage=params.get("f_2_reduced_tillage"),
1546
- f_2_no_tillage=params.get("f_2_no_tillage"),
1547
- f_2_unknown_tillage=params.get("f_2_unknown_tillage"),
1548
- )
1549
- for till in annual_tillage_categories
1550
- ]
1551
-
1552
- annual_tillage_factors = [
1553
- _get_tillage_factor(
1554
- till,
1555
- tillage_factor_full_tillage=params.get("tillage_factor_full_tillage"),
1556
- tillage_factor_reduced_tillage=params.get("tillage_factor_reduced_tillage"),
1557
- tillage_factor_no_tillage=params.get("tillage_factor_no_tillage"),
1558
- )
1559
- for till in annual_tillage_categories
1560
- ]
1561
-
1562
- # --- SPLIT ANNUAL DATA INTO RUN-IN AND INVENTORY PERIODS ---
1563
-
1564
- inventory_temperature_factors = timeseries_to_inventory(annual_temperature_factors, run_in_period)
1565
- inventory_water_factors = timeseries_to_inventory(annual_water_factors, run_in_period)
1566
- inventory_carbon_inputs = timeseries_to_inventory(annual_organic_carbon_inputs, run_in_period)
1567
- inventory_n_contents = timeseries_to_inventory(annual_n_contents, run_in_period)
1568
- inventory_lignin_contents = timeseries_to_inventory(annual_lignin_contents, run_in_period)
1569
- inventory_f_2s = timeseries_to_inventory(annual_f_2s, run_in_period)
1570
- inventory_tillage_factors = timeseries_to_inventory(annual_tillage_factors, run_in_period)
1571
-
1572
- # The last year of the run-in should be the first year of the inventory
1573
- inventory_timestamps = timestamps[run_in_period - 1:]
1574
-
1575
- # --- CALCULATE THE ACTIVE ACTIVE POOL STEADY STATES ---
1576
-
1577
- inventory_alphas = [
1578
- _calc_alpha(
1579
- carbon_input,
1580
- f_2,
1581
- f_4,
1582
- lignin_content,
1583
- nitrogen_content,
1584
- f_1=params.get("f_1"),
1585
- f_3=params.get("f_3"),
1586
- f_5=params.get("f_5"),
1587
- f_6=params.get("f_6"),
1588
- f_7=params.get("f_7"),
1589
- f_8=params.get("f_8"),
1590
- )
1591
- for carbon_input, f_2, lignin_content, nitrogen_content in zip(
1592
- inventory_carbon_inputs,
1593
- inventory_f_2s,
1594
- inventory_lignin_contents,
1595
- inventory_n_contents,
1596
- )
1597
- ]
1598
-
1599
- inventory_active_pool_decay_rates = [
1600
- _calc_active_pool_decay_rate(
1601
- temp_fac,
1602
- water_fac,
1603
- till_fac,
1604
- sand_content,
1605
- active_decay_factor=params.get("active_decay_factor"),
1606
- )
1607
- for temp_fac, water_fac, till_fac in zip(
1608
- inventory_temperature_factors,
1609
- inventory_water_factors,
1610
- inventory_tillage_factors,
1611
- )
1612
- ]
1613
-
1614
- inventory_active_pool_steady_states = [
1615
- _calc_active_pool_steady_state(alpha, active_decay_rate)
1616
- for alpha, active_decay_rate in zip(
1617
- inventory_alphas, inventory_active_pool_decay_rates
1618
- )
1619
- ]
1620
-
1621
- # --- CALCULATE THE SLOW POOL STEADY STATES ---
1622
-
1623
- inventory_slow_pool_decay_rates = [
1624
- _calc_slow_pool_decay_rate(
1625
- temp_fac, water_fac, till_fac, slow_decay_factor=params.get("slow_decay_factor")
1626
- )
1627
- for temp_fac, water_fac, till_fac in zip(
1628
- inventory_temperature_factors,
1629
- inventory_water_factors,
1630
- inventory_tillage_factors,
1631
- )
1632
- ]
1633
-
1634
- inventory_slow_pool_steady_states = [
1635
- _calc_slow_pool_steady_state(
1636
- carbon_input,
1637
- f_4,
1638
- active_steady_state,
1639
- active_decay_rate,
1640
- slow_decay_rate,
1641
- lignin_content,
1642
- f_3=params.get("f_3"),
1643
- )
1644
- for carbon_input, active_steady_state, active_decay_rate, slow_decay_rate, lignin_content in zip(
1645
- inventory_carbon_inputs,
1646
- inventory_active_pool_steady_states,
1647
- inventory_active_pool_decay_rates,
1648
- inventory_slow_pool_decay_rates,
1649
- inventory_lignin_contents,
1650
- )
1651
- ]
1652
-
1653
- # --- CALCULATE THE PASSIVE POOL STEADY STATES ---
1654
-
1655
- inventory_passive_pool_decay_rates = [
1656
- _calc_passive_pool_decay_rate(
1657
- temp_fac, water_fac, passive_decay_factor=params.get("passive_decay_factor")
1658
- )
1659
- for temp_fac, water_fac in zip(
1660
- inventory_temperature_factors, inventory_water_factors
1661
- )
1662
- ]
1663
-
1664
- inventory_passive_pool_steady_states = [
1665
- _calc_passive_pool_steady_state(
1666
- active_steady_state,
1667
- slow_steady_state,
1668
- active_decay_rate,
1669
- slow_decay_rate,
1670
- passive_decay_rate,
1671
- f_5=params.get("f_5"),
1672
- f_6=params.get("f_6"),
1673
- )
1674
- for active_steady_state, slow_steady_state, active_decay_rate, slow_decay_rate, passive_decay_rate in zip(
1675
- inventory_active_pool_steady_states,
1676
- inventory_slow_pool_steady_states,
1677
- inventory_active_pool_decay_rates,
1678
- inventory_slow_pool_decay_rates,
1679
- inventory_passive_pool_decay_rates,
1680
- )
1681
- ]
1682
-
1683
- # --- CALCULATE THE ACTIVE, SLOW AND PASSIVE SOC STOCKS ---
1684
-
1685
- inventory_active_pool_soc_stocks = inventory_active_pool_steady_states[:1]
1686
- inventory_slow_pool_soc_stocks = inventory_slow_pool_steady_states[:1]
1687
- inventory_passive_pool_soc_stocks = inventory_passive_pool_steady_states[:1]
1688
-
1689
- for index in range(1, len(inventory_timestamps), 1):
1690
- inventory_active_pool_soc_stocks.insert(
1691
- index,
1692
- _calc_sub_pool_soc_stock(
1693
- inventory_active_pool_steady_states[index],
1694
- inventory_active_pool_soc_stocks[index - 1],
1695
- inventory_active_pool_decay_rates[index],
1696
- ),
1697
- )
1698
- inventory_slow_pool_soc_stocks.insert(
1699
- index,
1700
- _calc_sub_pool_soc_stock(
1701
- inventory_slow_pool_steady_states[index],
1702
- inventory_slow_pool_soc_stocks[index - 1],
1703
- inventory_slow_pool_decay_rates[index],
1704
- ),
1705
- )
1706
- inventory_passive_pool_soc_stocks.insert(
1707
- index,
1708
- _calc_sub_pool_soc_stock(
1709
- inventory_passive_pool_steady_states[index],
1710
- inventory_passive_pool_soc_stocks[index - 1],
1711
- inventory_passive_pool_decay_rates[index],
1712
- ),
1713
- )
1714
-
1715
- # --- RETURN THE RESULT ---
1716
-
1717
- return Tier2SocResult(
1718
- timestamps=inventory_timestamps,
1719
- active_pool_soc_stocks=inventory_active_pool_soc_stocks,
1720
- slow_pool_soc_stocks=inventory_slow_pool_soc_stocks,
1721
- passive_pool_soc_stocks=inventory_passive_pool_soc_stocks,
1722
- )
1723
-
1724
-
1725
- # --- TIER 2 SUB-MODEL: ANNUAL TEMPERATURE FACTORS ---
1726
-
1727
-
1728
- def _check_12_months(inner_dict: dict, keys: set[Any]):
1729
- """
1730
- Checks whether an inner dict has 12 months of data for each of the required inner keys.
1731
-
1732
- Parameters
1733
- ----------
1734
- inner_dict : dict
1735
- A dictionary representing one year in a timeseries for the Tier 2 model.
1736
- keys : set[Any]
1737
- The required inner keys.
1738
-
1739
- Returns
1740
- -------
1741
- bool
1742
- Whether or not the inner dict satisfies the conditions.
1743
- """
1744
- return all(
1745
- len(inner_dict.get(key, [])) == 12 for key in keys
1746
- )
1747
-
1748
-
1749
- # --- SUB-MODEL ANNUAL TEMPERATURE FACTORS ---
1750
-
1751
-
1752
- def _run_annual_temperature_factors(
1753
- timestamps: list[int],
1754
- temperatures: list[list[float]],
1755
- maximum_temperature: float = 45.0,
1756
- optimum_temperature: float = 33.69,
1757
- ):
1758
- """
1759
- Parameters
1760
- ----------
1761
- timestamps : list[int]
1762
- A list of integer timestamps (e.g. `[1995, 1996]`) for each year in the inventory.
1763
- temperatures : list[list[float]])
1764
- A list of monthly average temperatures for each year in the inventory
1765
- (e.g. `[[10,10,10,20,25,15,15,10,10,10,5,5]]`).
1766
- maximum_temperature : float
1767
- The maximum air temperature for decomposition, degrees C, default value: `45.0`.
1768
- optimum_temperature : float
1769
- The optimum air temperature for decomposition, degrees C, default value: `33.69`.
1770
-
1771
- Returns
1772
- -------
1773
- TemperatureFactorResult
1774
- An inventory of annual temperature factor data as a named tuple with the format
1775
- `(timestamps: list[int], annual_temperature_factors: list[float])`.
1776
- """
1777
- return TemperatureFactorResult(
1778
- timestamps=timestamps,
1779
- annual_temperature_factors=[
1780
- _calc_annual_temperature_factor(
1781
- monthly_temperatures, maximum_temperature, optimum_temperature
1782
- )
1783
- for monthly_temperatures in temperatures
1784
- ],
1785
- )
1786
-
1787
-
1788
- # --- TIER 2 SUB-MODEL: ANNUAL WATER FACTORS ---
1789
-
1790
-
1791
- def _run_annual_water_factors(
1792
- timestamps: list[int],
1793
- precipitations: list[list[float]],
1794
- pets: list[list[float]],
1795
- is_irrigateds: Union[list[list[bool]], None] = None,
1796
- water_factor_slope: float = 1.331,
1797
- ):
1798
- """
1799
- Parameters
1800
- ----------
1801
- timestamps : list[int]
1802
- A list of integer timestamps (e.g. `[1995, 1996...]`) for each year in the inventory.
1803
- precipitations : list[list[float]]
1804
- A list of monthly sum precipitations for each year in the inventory
1805
- (e.g. `[[10,10,10,20,25,15,15,10,10,10,5,5]]`).
1806
- pets list[list[float]]
1807
- A list of monthly sum potential evapotransiprations for each year in the inventory.
1808
- is_irrigateds list[list[bool]] | None
1809
- A list of monthly booleans that describe whether irrigation is used in a particular calendar month for each
1810
- year in the inventory.
1811
- water_factor_slope : float
1812
- The slope for mappet term to estimate water factor, dimensionless, default value: `1.331`.
1813
-
1814
- Returns
1815
- -------
1816
- WaterFactorResult
1817
- An inventory of annual water factor data as a named tuple with the format
1818
- `(timestamps: list[int], annual_water_factors: list[float])`.
1819
- """
1820
- is_irrigateds = [None] * len(timestamps) if is_irrigateds is None else is_irrigateds
1821
- return WaterFactorResult(
1822
- timestamps=timestamps,
1823
- annual_water_factors=[
1824
- _calc_annual_water_factor(
1825
- monthly_precipitations,
1826
- monthly_pets,
1827
- monthly_is_irrigateds,
1828
- water_factor_slope,
1829
- )
1830
- for monthly_precipitations, monthly_pets, monthly_is_irrigateds in zip(
1831
- precipitations, pets, is_irrigateds
1832
- )
1833
- ],
1834
- )
1835
-
1836
-
1837
- # --- TIER 2 SUB-MODEL: ANNUAL ORGANIC CARBON INPUTS ---
1838
-
1839
-
1840
- def _iterate_carbon_source(node: dict) -> Union[CarbonSource, None]:
1841
- """
1842
- Validates whether a node is a valid carbon source and returns
1843
- a `CarbonSource` named tuple if yes.
1844
-
1845
- Parameters
1846
- ----------
1847
- node : dict
1848
- A Hestia `Product` or `Input` node, see: https://www.hestia.earth/schema/Product
1849
- or https://www.hestia.earth/schema/Input.
1850
-
1851
- Returns
1852
- -------
1853
- CarbonSource | None
1854
- A `CarbonSource` named tuple if the node is a carbon source with the required properties, else `None`.
1855
- """
1856
- mass = list_sum(node.get("value", []))
1857
- carbon_content, nitrogen_content, lignin_content = (
1858
- get_node_property(node, term_id).get("value", 0)/100 for term_id in CARBON_INPUT_PROPERTY_TERM_IDS
1859
- )
1860
-
1861
- should_run = all([
1862
- mass > 0,
1863
- 0 < carbon_content <= 1,
1864
- 0 < nitrogen_content <= 1,
1865
- 0 < lignin_content <= 1
1866
- ])
1867
-
1868
- return (
1869
- CarbonSource(
1870
- mass, carbon_content, nitrogen_content, lignin_content
1871
- ) if should_run else None
1872
- )
1873
-
1874
-
1875
- def _get_carbon_sources_from_cycles(cycles: dict) -> list[CarbonSource]:
1876
- """
1877
- Retrieves and formats all of the valid carbon sources from a list of cycles.
1878
-
1879
- Carbon sources can be either a Hestia `Product` node (e.g. crop residue) or `Input` node (e.g. organic amendment).
1880
-
1881
- Parameters
1882
- ----------
1883
- cycles : list[dict]
1884
- A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
1885
-
1886
- Returns
1887
- -------
1888
- list[CarbonSource]
1889
- A formatted list of `CarbonSource`s for the inputted `Cycle`s.
1890
- """
1891
- inputs_and_products = non_empty_list(flatten(
1892
- [cycle.get("inputs", []) + cycle.get("products", []) for cycle in cycles]
1893
- ))
1894
- crop_residue_terms = get_crop_residue_incorporated_or_left_on_field_terms()
1895
-
1896
- return non_empty_list([
1897
- _iterate_carbon_source(node) for node in inputs_and_products
1898
- if any([
1899
- node.get("term", {}).get("@id") in crop_residue_terms,
1900
- node.get("term", {}).get("termType") in CARBON_SOURCE_TERM_TYPES
1901
- ])
1902
- ])
1903
-
1904
-
1905
- # --- TIER 2 SOC MODEL ---
1906
-
1907
-
1908
- def _run_tier_2(
1909
- inventory: dict[int: dict[_InventoryKey: any]],
1910
- *,
1911
- run_in_period: int = 5,
1912
- run_with_irrigation: bool = True,
1913
- sand_content: float = 0.33,
1914
- params: Union[dict[str, float], None] = None,
1915
- **_
1916
- ) -> list[dict]:
1917
- """
1918
- Run the IPCC Tier 2 SOC model on a time series of annual data about a site and the mangagement activities taking
1919
- place on it. To avoid any errors, the `inventory` parameter must be pre-validated by the `should_run` function.
1920
-
1921
- The inventory should be in the following shape:
1922
- ```
1923
- {
1924
- year (int): {
1925
- _InventoryKey.SHOULD_RUN_TIER_2: bool,
1926
- _InventoryKey.TEMP_MONTHLY: list[float],
1927
- _InventoryKey.PRECIP_MONTHLY: list[float],
1928
- _InventoryKey.PET_MONTHLY: list[float],
1929
- _InventoryKey.IRRIGATED_MONTHLY: list[bool]
1930
- _InventoryKey.CARBON_INPUT: float,
1931
- _InventoryKey.N_CONTENT: float,
1932
- _InventoryKey.TILLAGE_CATEGORY: IpccManagementCategory,
1933
- _InventoryKey.SAND_CONTENT: float
1934
- },
1935
- ...
1936
- }
1937
- ```
1938
-
1939
- TODO: interpolate between `sandContent` measurements for different years of the inventory
1940
-
1941
- Parameters
1942
- ----------
1943
- inventory : dict
1944
- The inventory built by the `_should_run` function.
1945
- run_in_period : int, optional
1946
- The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
1947
- run_with_irrigation : bool, optional
1948
- `True` if the model should run while taking into account irrigation, `False` if not.
1949
- sand_content : float, optional
1950
- A back-up sand content for if none are found in the inventory, decimal proportion, default value: `0.33`.
1951
- params : dict | None, optional
1952
- Overrides for the model parameters. If `None` only default parameters will be used.
1953
-
1954
- Returns
1955
- -------
1956
- list[dict]
1957
- A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
1958
- """
1959
- valid_inventory = {
1960
- year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_2)
1961
- }
1962
-
1963
- timestamps = [year for year in valid_inventory.keys()]
1964
-
1965
- annual_temperature_monthlys = [group[_InventoryKey.TEMP_MONTHLY] for group in valid_inventory.values()]
1966
- annual_precipitation_monthlys = [group[_InventoryKey.PRECIP_MONTHLY] for group in valid_inventory.values()]
1967
- annual_pet_monthlys = [group[_InventoryKey.PET_MONTHLY] for group in valid_inventory.values()]
1968
-
1969
- annual_carbon_inputs = [group[_InventoryKey.CARBON_INPUT] for group in valid_inventory.values()]
1970
- annual_n_contents = [group[_InventoryKey.N_CONTENT] for group in valid_inventory.values()]
1971
- annual_lignin_contents = [group[_InventoryKey.LIGNIN_CONTENT] for group in valid_inventory.values()]
1972
- annual_tillage_categories = [group[_InventoryKey.TILLAGE_CATEGORY] for group in valid_inventory.values()]
1973
- annual_irrigated_monthly = (
1974
- [group[_InventoryKey.IRRIGATED_MONTHLY] for group in valid_inventory.values()] if run_with_irrigation else None
1975
- )
1976
-
1977
- sand_content = next(
1978
- (
1979
- group[_InventoryKey.SAND_CONTENT] for group in valid_inventory.values()
1980
- if _InventoryKey.SAND_CONTENT in group
1981
- ),
1982
- sand_content
1983
- )
1984
-
1985
- # --- MERGE ANY USER-SET PARAMETERS WITH THE IPCC DEFAULTS ---
1986
-
1987
- params = DEFAULT_PARAMS | (params or {})
1988
-
1989
- # --- COMPUTE FACTORS AND CARBON INPUTS ---
1990
-
1991
- _, annual_temperature_factors = _run_annual_temperature_factors(
1992
- timestamps,
1993
- annual_temperature_monthlys,
1994
- maximum_temperature=params.get("maximum_temperature"),
1995
- optimum_temperature=params.get("optimum_temperature")
1996
- )
1997
-
1998
- _, annual_water_factors = _run_annual_water_factors(
1999
- timestamps,
2000
- annual_precipitation_monthlys,
2001
- annual_pet_monthlys,
2002
- annual_irrigated_monthly,
2003
- water_factor_slope=params.get("water_factor_slope")
2004
- )
2005
-
2006
- # --- RUN THE MODEL ---
2007
-
2008
- result = _run_soc_stocks(
2009
- timestamps=timestamps,
2010
- annual_temperature_factors=annual_temperature_factors,
2011
- annual_water_factors=annual_water_factors,
2012
- annual_organic_carbon_inputs=annual_carbon_inputs,
2013
- annual_n_contents=annual_n_contents,
2014
- annual_lignin_contents=annual_lignin_contents,
2015
- annual_tillage_categories=annual_tillage_categories,
2016
- sand_content=sand_content,
2017
- run_in_period=run_in_period,
2018
- params=params
2019
- )
2020
-
2021
- values = [
2022
- _calc_tier_2_soc_stock(
2023
- active,
2024
- slow,
2025
- passive
2026
- ) for active, slow, passive in zip(
2027
- result.active_pool_soc_stocks,
2028
- result.slow_pool_soc_stocks,
2029
- result.passive_pool_soc_stocks
2030
- )
2031
- ]
2032
-
2033
- # --- RETURN MEASUREMENT NODES ---
2034
-
2035
- return [
2036
- _measurement(
2037
- year,
2038
- value,
2039
- MeasurementMethodClassification.TIER_2_MODEL.value
2040
- ) for year, value in zip(
2041
- result.timestamps,
2042
- values
2043
- )
2044
- ]
2045
-
2046
-
2047
- # --- TIER 1 FUNCTIONS ---
2048
-
2049
-
2050
- def _retrieve_soc_ref(
2051
- eco_climate_zone: int,
2052
- ipcc_soil_category: IpccSoilCategory
2053
- ) -> float:
2054
- """
2055
- Retrieve the soil organic carbon (SOC) reference value for a given combination of eco-climate zone
2056
- and IPCC soil category.
2057
-
2058
- See [IPCC (2019) Vol. 4, Ch. 2, Table 2.3](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html)
2059
- for more information.
2060
-
2061
- Parameters
2062
- ----------
2063
- eco_climate_zone : int
2064
- The eco-climate zone identifier for the site corresponding to a row in the
2065
- [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
2066
- lookup table.
2067
- ipcc_soil_category : IpccSoilCategory
2068
- The IPCC soil category of the site.
2069
-
2070
- Returns
2071
- -------
2072
- float
2073
- The reference condition soil organic carbon (SOC) stock in the 0-30cm depth interval, kg C ha-1.
2074
- """
2075
- col_name = _get_eco_climate_zone_lookup_column(ipcc_soil_category)
2076
- return get_ecoClimateZone_lookup_value(eco_climate_zone, col_name)
2077
-
2078
-
2079
- def _retrieve_soc_stock_factors(
2080
- eco_climate_zone: int,
2081
- ipcc_land_use_category: IpccLandUseCategory,
2082
- ipcc_management_category: IpccManagementCategory,
2083
- ipcc_carbon_input_category: IpccCarbonInputCategory
2084
- ) -> StockChangeFactors:
2085
- """
2086
- Retrieve the stock change factors for soil organic carbon (SOC) based on a given combination of land use,
2087
- management and carbon input.
2088
-
2089
- Parameters
2090
- ----------
2091
- eco_climate_zone : int
2092
- The eco-climate zone identifier for the site corresponding to a row in the
2093
- [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
2094
- lookup table.
2095
- ipcc_land_use_category : IpccLandUseCategory
2096
- The IPCC land use category for the inventory year.
2097
- ipcc_management_category : IpccManagementCategory
2098
- The IPCC land use category for the inventory year.
2099
- ipcc_carbon_input_category : IpccCarbonInputCategory
2100
- The IPCC land use category for the inventory year.
2101
-
2102
- Returns
2103
- -------
2104
- StockChangeFactors
2105
- A named tuple containing the retrieved stock change factors for SOC.
2106
- """
2107
- DEFAULT_FACTOR = 1
2108
-
2109
- EXCLUDED_LAND_USE_CATEGORIES = {
2110
- IpccLandUseCategory.FOREST,
2111
- IpccLandUseCategory.NATIVE,
2112
- IpccLandUseCategory.OTHER
2113
- }
2114
-
2115
- EXCLUDED_MANAGEMENT_CATEGORIES = {
2116
- IpccManagementCategory.OTHER
2117
- }
2118
-
2119
- EXCLUDED_CARBON_INPUT_CATEGORIES = {
2120
- IpccCarbonInputCategory.OTHER
2121
- }
2122
-
2123
- def get_factor(category, exclude_set):
2124
- return (
2125
- DEFAULT_FACTOR if category in exclude_set
2126
- else get_ecoClimateZone_lookup_value(
2127
- eco_climate_zone, _get_eco_climate_zone_lookup_column(category)
2128
- )
2129
- )
2130
-
2131
- land_use_factor = get_factor(ipcc_land_use_category, EXCLUDED_LAND_USE_CATEGORIES)
2132
- management_factor = get_factor(ipcc_management_category, EXCLUDED_MANAGEMENT_CATEGORIES)
2133
- carbon_input_factor = get_factor(ipcc_carbon_input_category, EXCLUDED_CARBON_INPUT_CATEGORIES)
2134
-
2135
- return StockChangeFactors(land_use_factor, management_factor, carbon_input_factor)
2136
-
2137
-
2138
- def _calc_soc_equilibrium(
2139
- soc_ref: float,
2140
- land_use_factor: float,
2141
- management_factor: float,
2142
- carbon_input_factor: float
2143
- ) -> float:
2144
- """
2145
- Calculate the soil organic carbon (SOC) equilibrium based on reference SOC and factors.
2146
-
2147
- In the tier 1 model, SOC equilibriums are considered to be reached after 20 years of consistant land use,
2148
- management and carbon input.
2149
-
2150
- Parameters
2151
- ----------
2152
- soc_ref : float
2153
- The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
2154
- land_use_factor : float
2155
- The stock change factor for mineral soil organic C land-use systems or sub-systems
2156
- for a particular land-use, dimensionless.
2157
- management_factor : float
2158
- The stock change factor for mineral soil organic C for management regime, dimensionless.
2159
- carbon_input_factor : float
2160
- The stock change factor for mineral soil organic C for the input of organic amendments, dimensionless.
2161
-
2162
- Returns
2163
- -------
2164
- float
2165
- The calculated SOC equilibrium, kg C ha-1.
2166
- """
2167
- return soc_ref * land_use_factor * management_factor * carbon_input_factor
2168
-
2169
-
2170
- def _calc_regime_start_index(
2171
- current_index: int, soc_equilibriums: list[float], default: Optional[int] = None
2172
- ) -> Optional[int]:
2173
- """
2174
- Calculate the start index of the SOC regime based on the current index and equilibriums.
2175
-
2176
- Parameters
2177
- ----------
2178
- current_index : int
2179
- The current index in the SOC equilibriums list.
2180
- soc_equilibriums : list[float]
2181
- List of SOC equilibriums.
2182
- default : Any | None
2183
- Default value to return if no suitable start index is found, by default `None`.
2184
-
2185
- Returns
2186
- -------
2187
- int | None
2188
- The calculated start index for the SOC regime.
2189
- """
2190
-
2191
- def calc_forward_index(sliced_reverse_index: int) -> int:
2192
- """
2193
- Calculate the forward index based on a sliced reverse index.
2194
- """
2195
- return current_index - sliced_reverse_index - 1
2196
-
2197
- current_soc_equilibrium = soc_equilibriums[current_index]
2198
- sliced_reversed_soc_equilibriums = reversed(soc_equilibriums[0:current_index])
2199
-
2200
- return next(
2201
- (
2202
- calc_forward_index(sliced_reverse_index) for sliced_reverse_index, prev_equilibrium
2203
- in enumerate(sliced_reversed_soc_equilibriums)
2204
- if not prev_equilibrium == current_soc_equilibrium
2205
- ),
2206
- default
2207
- )
2208
-
2209
-
2210
- def _iterate_soc_equilibriums(
2211
- timestamps: list[int], soc_equilibriums: list[float]
2212
- ) -> tuple[list[int], list[float]]:
2213
- """
2214
- Iterate over SOC equilibriums, inserting timestamps and soc_equilibriums for any missing years where SOC would have
2215
- reached equilibrium.
2216
-
2217
- Parameters
2218
- ----------
2219
- timestamps : list[int]
2220
- List of timestamps for each year in the inventory.
2221
- soc_equilibriums : list[float]
2222
- List of SOC equilibriums for each year in the inventory.
2223
-
2224
- Returns
2225
- -------
2226
- tuple[list[int], list[float]]
2227
- Updated `timestamps` and `soc_equilibriums`.
2228
- """
2229
- iterated_timestamps = list(timestamps)
2230
- iterated_soc_equilibriums = list(soc_equilibriums)
2231
-
2232
- def calc_equilibrium_reached_timestamp(index: int) -> int:
2233
- """
2234
- Calculate the timestamp when SOC equilibrium is reached based on the current index.
2235
- """
2236
- regime_start_index = _calc_regime_start_index(index, soc_equilibriums)
2237
- regime_start_timestamp = (
2238
- timestamps[regime_start_index] if regime_start_index is not None
2239
- else timestamps[0] - EQUILIBRIUM_TRANSITION_PERIOD
2240
- )
2241
- return regime_start_timestamp + EQUILIBRIUM_TRANSITION_PERIOD
2242
-
2243
- def is_missing_equilibrium_year(
2244
- timestamp: int, equilibrium_reached_timestamp: int
2245
- ) -> bool:
2246
- """
2247
- Check if the given timestamp is after equilibrium and the equilibrium year is missing.
2248
- """
2249
- return (
2250
- timestamp > equilibrium_reached_timestamp
2251
- and equilibrium_reached_timestamp not in iterated_timestamps
2252
- )
2253
-
2254
- for index, (timestamp, soc_equilibrium) in enumerate(zip(timestamps, soc_equilibriums)):
2255
- equilibrium_reached_timestamp = calc_equilibrium_reached_timestamp(index)
2256
-
2257
- if is_missing_equilibrium_year(timestamp, equilibrium_reached_timestamp):
2258
- iterated_timestamps.insert(index, equilibrium_reached_timestamp)
2259
- iterated_soc_equilibriums.insert(index, soc_equilibrium)
2260
-
2261
- return iterated_timestamps, iterated_soc_equilibriums
2262
-
2263
-
2264
- def _run_soc_equilibriums(
2265
- timestamps: list[int],
2266
- ipcc_land_use_categories: list[IpccLandUseCategory],
2267
- ipcc_management_categories: list[IpccManagementCategory],
2268
- ipcc_carbon_input_categories: list[IpccCarbonInputCategory],
2269
- eco_climate_zone: int,
2270
- soc_ref: float
2271
- ) -> tuple[list[int], list[float]]:
2272
- """
2273
- Run the soil organic carbon (SOC) equilibriums calculation for each year in the inventory.
2274
-
2275
- Missing years where SOC equilibrium would be reached are inserted to allow for annual SOC change to be calculated
2276
- correctly.
2277
-
2278
- Parameters
2279
- ----------
2280
- timestamps : list[int]
2281
- A list of timestamps for each year in the inventory.
2282
- ipcc_land_use_categories : list[IpccLandUseCategory]
2283
- A list of IPCC land use categories for each year in the inventory.
2284
- ipcc_management_categories : list[IpccManagementCategory]
2285
- A list of IPCC management categories for each year in the inventory.
2286
- ipcc_carbon_input_categories : list[IpccCarbonInputCategory]
2287
- A list of IPCC carbon input categories for each year in the inventory.
2288
- eco_climate_zone : int
2289
- The eco-climate zone identifier for the site corresponding to a row in the
2290
- [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
2291
- lookup table.
2292
- soc_ref : float
2293
- The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
2294
-
2295
- Returns
2296
- -------
2297
- tuple[list[int], list[float]]
2298
- `timestamps` and `soc_equilibriums` for each year in the inventory, including any missing years where SOC
2299
- equilibrium would have been reached.
2300
- """
2301
-
2302
- # Calculate SOC equilibriums for each year
2303
- soc_equilibriums = [
2304
- _calc_soc_equilibrium(
2305
- soc_ref,
2306
- *_retrieve_soc_stock_factors(
2307
- eco_climate_zone,
2308
- land_use_category,
2309
- management_category,
2310
- carbon_input_category
2311
- )
2312
- ) for land_use_category, management_category, carbon_input_category in zip(
2313
- ipcc_land_use_categories,
2314
- ipcc_management_categories,
2315
- ipcc_carbon_input_categories
2316
- )
2317
- ]
2318
-
2319
- # Insert missing years where SOC equilibrium would have been reached
2320
- iterated_timestamps, iterated_soc_equilibriums = (
2321
- _iterate_soc_equilibriums(timestamps, soc_equilibriums)
2322
- )
2323
-
2324
- return iterated_timestamps, iterated_soc_equilibriums
2325
-
2326
-
2327
- def _calc_tier_1_soc_stocks(
2328
- timestamps: list[int],
2329
- soc_equilibriums: list[float],
2330
- ) -> list[float]:
2331
- """
2332
- Calculate soil organic carbon (SOC) stocks (kg C ha-1) in the 0-30cm depth interval for each year in the inventory.
2333
-
2334
- Parameters
2335
- ----------
2336
- timestamps : list[int]
2337
- A list of timestamps for each year in the inventory.
2338
- soc_equilibriums : list[float]
2339
- A list of SOC equilibriums for each year in the inventory.
2340
-
2341
- Returns
2342
- -------
2343
- list[float]
2344
- SOC stocks for each year in the inventory.
2345
- """
2346
- soc_stocks = [soc_equilibriums[0]]
2347
-
2348
- for index in range(1, len(soc_equilibriums)):
2349
-
2350
- timestamp = timestamps[index]
2351
- soc_equilibrium = soc_equilibriums[index]
2352
-
2353
- regime_start_index = _calc_regime_start_index(index, soc_equilibriums)
2354
-
2355
- regime_start_timestamp = (
2356
- timestamps[regime_start_index]
2357
- if regime_start_index is not None
2358
- else timestamps[0] - EQUILIBRIUM_TRANSITION_PERIOD
2359
- )
2360
-
2361
- regime_start_soc_stock = soc_stocks[regime_start_index or 0]
2362
-
2363
- regime_duration = timestamp - regime_start_timestamp
2364
-
2365
- time_ratio = min(regime_duration / EQUILIBRIUM_TRANSITION_PERIOD, 1)
2366
- soc_delta = (soc_equilibrium - regime_start_soc_stock) * time_ratio
2367
-
2368
- soc_stocks.append(regime_start_soc_stock + soc_delta)
2369
-
2370
- return soc_stocks
2371
-
2372
-
2373
- # --- GET THE ECO-CLIMATE ZONE FROM THE MEASUREMENTS ---
2374
-
2375
-
2376
- def _get_eco_climate_zone(measurements: list[dict]) -> Optional[int]:
2377
- """
2378
- Get the eco-climate zone value from a list of measurements.
2379
-
2380
- Parameters
2381
- ----------
2382
- measurements : list[dict]
2383
- A list of measurement nodes.
2384
-
2385
- Returns
2386
- -------
2387
- int | None
2388
- The eco-climate zone value if found, otherwise None.
2389
- """
2390
- eco_climate_zone = find_term_match(measurements, "ecoClimateZone")
2391
- return get_node_value(eco_climate_zone) or None
2392
-
2393
-
2394
- # --- ASSIGN IPCC SOIL CATEGORY TO SITE ---
2395
-
2396
-
2397
- def _check_soil_category(
2398
- *,
2399
- key: IpccSoilCategory,
2400
- soil_types: list[dict],
2401
- usda_soil_types: list[dict],
2402
- **_
2403
- ) -> bool:
2404
- """
2405
- Check if the soil category matches the given key.
2406
-
2407
- Parameters
2408
- ----------
2409
- key : IpccSoilCategory
2410
- The IPCC soil category to check.
2411
- soil_types : list[dict]
2412
- List of soil type measurement nodes.
2413
- usda_soil_types : list[dict]
2414
- List of USDA soil type measurement nodes
2415
-
2416
- Returns
2417
- -------
2418
- bool
2419
- `True` if the soil category matches, `False` otherwise.
2420
- """
2421
- SOIL_TYPE_LOOKUP = LOOKUPS["soilType"]
2422
- USDA_SOIL_TYPE_LOOKUP = LOOKUPS["usdaSoilType"]
2423
-
2424
- target_lookup_values = IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE.get(key, None)
2425
-
2426
- is_soil_type_match = cumulative_nodes_lookup_match(
2427
- soil_types,
2428
- lookup=SOIL_TYPE_LOOKUP,
2429
- target_lookup_values=target_lookup_values,
2430
- cumulative_threshold=MIN_AREA_THRESHOLD
2431
- )
2432
-
2433
- is_usda_soil_type_match = cumulative_nodes_lookup_match(
2434
- usda_soil_types,
2435
- lookup=USDA_SOIL_TYPE_LOOKUP,
2436
- target_lookup_values=target_lookup_values,
2437
- cumulative_threshold=MIN_AREA_THRESHOLD
2438
- )
2439
-
2440
- return is_soil_type_match or is_usda_soil_type_match
2441
-
2442
-
2443
- def _check_sandy_soil_category(
2444
- *,
2445
- key: IpccSoilCategory,
2446
- soil_types: list[dict],
2447
- usda_soil_types: list[dict],
2448
- has_sandy_soil: bool,
2449
- **_
2450
- ) -> bool:
2451
- """
2452
- Check if the soils are sandy.
2453
-
2454
- This function is special case of `_check_soil_category`.
2455
-
2456
- Parameters
2457
- ----------
2458
- key : IpccSoilCategory
2459
- The IPCC soil category to check.
2460
- soil_types : list[dict]
2461
- List of soil type measurement nodes.
2462
- usda_soil_types : list[dict]
2463
- List of USDA soil type measurement nodes
2464
- has_sandy_soil : bool
2465
- True if the soils are sandy, False otherwise.
2466
-
2467
- Returns
2468
- -------
2469
- bool
2470
- `True` if the soil category matches, `False` otherwise.
2471
- """
2472
- return _check_soil_category(key=key, soil_types=soil_types, usda_soil_types=usda_soil_types) or has_sandy_soil
2473
-
2474
-
2475
- SOIL_CATEGORY_DECISION_TREE = {
2476
- IpccSoilCategory.ORGANIC_SOILS: _check_soil_category,
2477
- IpccSoilCategory.SANDY_SOILS: _check_sandy_soil_category,
2478
- IpccSoilCategory.WETLAND_SOILS: _check_soil_category,
2479
- IpccSoilCategory.VOLCANIC_SOILS: _check_soil_category,
2480
- IpccSoilCategory.SPODIC_SOILS: _check_soil_category,
2481
- IpccSoilCategory.HIGH_ACTIVITY_CLAY_SOILS: _check_soil_category,
2482
- IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS: _check_soil_category
2483
- }
2484
- """
2485
- A decision tree mapping IPCC soil categories to corresponding check functions.
2486
-
2487
- Key: IpccSoilCategory
2488
- Value: Corresponding function for checking the match of the given soil category based on soil types.
2489
- """
2490
-
2491
-
2492
- def _assign_ipcc_soil_category(
2493
- measurements: list[dict],
2494
- default: IpccSoilCategory = IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS
2495
- ) -> IpccSoilCategory:
2496
- """
2497
- Assign an IPCC soil category based on a site"s measurement nodes.
2498
-
2499
- Parameters
2500
- ----------
2501
- measurements : list[dict]
2502
- List of measurement nodes.
2503
- default : IpccSoilCategory, optional
2504
- The default soil category if none matches, by default IpccSoilCategory.LOW_ACTIVITY_CLAY_SOILS.
2505
-
2506
- Returns
2507
- -------
2508
- IpccSoilCategory
2509
- The assigned IPCC soil category.
2510
- """
2511
- soil_types = filter_list_term_type(measurements, TermTermType.SOILTYPE)
2512
- usda_soil_types = filter_list_term_type(measurements, TermTermType.USDASOILTYPE)
2513
-
2514
- clay_content = get_node_value(find_term_match(measurements, CLAY_CONTENT_TERM_ID))
2515
- sand_content = get_node_value(find_term_match(measurements, SAND_CONTENT_TERM_ID))
2516
-
2517
- has_sandy_soil = clay_content < CLAY_CONTENT_MAX and sand_content > SAND_CONTENT_MIN
2518
-
2519
- return next(
2520
- (
2521
- key for key in SOIL_CATEGORY_DECISION_TREE
2522
- if SOIL_CATEGORY_DECISION_TREE[key](
2523
- key=key,
2524
- soil_types=soil_types,
2525
- usda_soil_types=usda_soil_types,
2526
- has_sandy_soil=has_sandy_soil
2527
- )
2528
- ),
2529
- default
2530
- ) if len(soil_types) > 0 or len(usda_soil_types) > 0 else default
2531
-
2532
-
2533
- # --- ASSIGN IPCC LAND USE CATEGORY ---
2534
-
2535
-
2536
- def _has_irrigation(water_regime_nodes: list[dict]) -> bool:
2537
- """
2538
- Check if irrigation is present in the water regime nodes.
2539
-
2540
- Parameters
2541
- ----------
2542
- water_regime_nodes : list[dict]
2543
- List of water regime nodes to be checked.
2544
-
2545
- Returns
2546
- -------
2547
- bool
2548
- `True` if irrigation is present, `False` otherwise.
2549
- """
2550
- return cumulative_nodes_term_match(
2551
- water_regime_nodes,
2552
- target_term_ids=get_irrigated_terms(),
2553
- cumulative_threshold=MIN_AREA_THRESHOLD
2554
- )
2555
-
2556
-
2557
- def _has_long_fallow(land_cover_nodes: list[dict]) -> bool:
2558
- """
2559
- Check if long fallow terms are present in the land cover nodes.
2560
-
2561
- n.b., a super majority of the site area must be under long fallow for it to be classified as set aside.
2562
-
2563
- Parameters
2564
- ----------
2565
- land_cover_nodes : list[dict]
2566
- List of land cover nodes to be checked.
2567
-
2568
- Returns
2569
- -------
2570
- bool
2571
- `True` if long fallow is present, `False` otherwise.
2572
- """
2573
- LOOKUP = LOOKUPS["landCover"][0]
2574
- TARGET_LOOKUP_VALUE = "Set aside"
2575
- return cumulative_nodes_lookup_match(
2576
- land_cover_nodes,
2577
- lookup=LOOKUP,
2578
- target_lookup_values=TARGET_LOOKUP_VALUE,
2579
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
2580
- ) or cumulative_nodes_match(
2581
- lambda node: get_node_property(node, LONG_FALLOW_CROP_TERM_ID, False).get("value", 0),
2582
- land_cover_nodes,
2583
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
2584
- )
2585
-
2586
-
2587
- def _has_upland_rice(land_cover_nodes: list[dict]) -> bool:
2588
- """
2589
- Check if upland rice is present in the land cover nodes.
2590
-
2591
- Parameters
2592
- ----------
2593
- land_cover_nodes : list[dict]
2594
- List of land cover nodes to be checked.
2595
-
2596
- Returns
2597
- -------
2598
- bool
2599
- `True` if upland rice is present, `False` otherwise.
2600
- """
2601
- return cumulative_nodes_term_match(
2602
- land_cover_nodes,
2603
- target_term_ids=get_upland_rice_land_cover_terms(),
2604
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
2605
- )
2606
-
2607
-
2608
- IPCC_LAND_USE_CATEGORY_TO_VALIDATION_KWARGS = {
2609
- IpccLandUseCategory.ANNUAL_CROPS_WET: {"has_wetland_soils"},
2610
- IpccLandUseCategory.SET_ASIDE: {"has_long_fallow"},
2611
- }
2612
- """
2613
- Keyword arguments that need to be validated in addition to the `landCover` lookup match for specific
2614
- `IpccLandUseCategory`s.
2615
- """
2616
-
2617
- IPCC_LAND_USE_CATEGORY_TO_OVERRIDE_KWARGS = {
2618
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: {"has_irrigated_upland_rice"}
2619
- }
2620
- """
2621
- Keyword arguments that can override the `landCover` lookup match for specific `IpccLandUseCategory`s.
2622
- """
2623
-
2624
-
2625
- def _check_ipcc_land_use_category(*, key: IpccLandUseCategory, land_cover_nodes: list[dict], **kwargs) -> bool:
2626
- """
2627
- Check if the land cover nodes and keyword args satisfy the requirements for the given key.
2628
-
2629
- Parameters
2630
- ----------
2631
- key : IpccLandUseCategory
2632
- The IPCC land use category to check.
2633
- land_cover_nodes : list[dict]
2634
- List of land cover nodes to be checked.
2635
-
2636
- Keyword Args
2637
- ------------
2638
- has_irrigated_upland_rice : bool
2639
- Indicates whether irrigated upland rice is present on more than 30% of the site.
2640
- has_long_fallow : bool
2641
- Indicates whether long fallow is present on more than 70% of the site.
2642
- has_wetland_soils : bool
2643
- Indicates whether wetland soils are present to more than 30% of the site.
2644
-
2645
- Returns
2646
- -------
2647
- bool
2648
- `True` if the conditions match the specified land use category, `False` otherwise.
2649
- """
2650
- LOOKUP = LOOKUPS["landCover"][0]
2651
- target_lookup_values = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(key, None)
2652
- valid_lookup = cumulative_nodes_lookup_match(
2653
- land_cover_nodes,
2654
- lookup=LOOKUP,
2655
- target_lookup_values=target_lookup_values,
2656
- cumulative_threshold=MIN_AREA_THRESHOLD
2657
- )
2658
-
2659
- validation_kwargs = IPCC_LAND_USE_CATEGORY_TO_VALIDATION_KWARGS.get(key, set())
2660
- valid_kwargs = all(v for k, v in kwargs.items() if k in validation_kwargs)
2661
-
2662
- override_kwargs = IPCC_LAND_USE_CATEGORY_TO_OVERRIDE_KWARGS.get(key, set())
2663
- valid_override = any(v for k, v in kwargs.items() if k in override_kwargs)
2664
-
2665
- return (valid_lookup and valid_kwargs) or valid_override
2666
-
2667
-
2668
- LAND_USE_CATEGORY_DECISION_TREE = {
2669
- IpccLandUseCategory.GRASSLAND: _check_ipcc_land_use_category,
2670
- IpccLandUseCategory.SET_ASIDE: _check_ipcc_land_use_category,
2671
- IpccLandUseCategory.PERENNIAL_CROPS: _check_ipcc_land_use_category,
2672
- IpccLandUseCategory.PADDY_RICE_CULTIVATION: _check_ipcc_land_use_category,
2673
- IpccLandUseCategory.ANNUAL_CROPS_WET: _check_ipcc_land_use_category,
2674
- IpccLandUseCategory.ANNUAL_CROPS: _check_ipcc_land_use_category,
2675
- IpccLandUseCategory.FOREST: _check_ipcc_land_use_category,
2676
- IpccLandUseCategory.NATIVE: _check_ipcc_land_use_category,
2677
- IpccLandUseCategory.OTHER: _check_ipcc_land_use_category
2678
- }
2679
- """
2680
- A decision tree mapping IPCC soil categories to corresponding check functions.
2681
-
2682
- Key: IpccLandUseCategory
2683
- Value: Corresponding function for checking the match of the given land use category based on land cover nodes
2684
- and additional kwargs.
2685
- """
2686
-
2687
-
2688
- def _assign_ipcc_land_use_category(
2689
- management_nodes: list[dict], ipcc_soil_category: IpccSoilCategory,
2690
- ) -> IpccLandUseCategory:
2691
- """
2692
- Assigns IPCC land use category based on management nodes and soil category.
2693
-
2694
- Parameters
2695
- ----------
2696
- management_nodes : list[dict]
2697
- List of management nodes.
2698
- ipcc_soil_category : IpccSoilCategory
2699
- The site"s assigned IPCC soil category.
2700
-
2701
- Returns
2702
- -------
2703
- IpccLandUseCategory
2704
- Assigned IPCC land use category.
2705
- """
2706
- DECISION_TREE = LAND_USE_CATEGORY_DECISION_TREE
2707
- DEFAULT = IpccLandUseCategory.OTHER
2708
-
2709
- land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
2710
- water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
2711
-
2712
- has_irrigation = _has_irrigation(water_regime_nodes)
2713
- has_upland_rice = _has_upland_rice(land_cover_nodes)
2714
- has_irrigated_upland_rice = has_upland_rice and has_irrigation
2715
- has_long_fallow = _has_long_fallow(land_cover_nodes)
2716
- has_wetland_soils = ipcc_soil_category is IpccSoilCategory.WETLAND_SOILS
2717
-
2718
- should_run = bool(land_cover_nodes)
2719
-
2720
- return next(
2721
- (
2722
- key for key in DECISION_TREE
2723
- if DECISION_TREE[key](
2724
- key=key,
2725
- land_cover_nodes=land_cover_nodes,
2726
- has_long_fallow=has_long_fallow,
2727
- has_irrigated_upland_rice=has_irrigated_upland_rice,
2728
- has_wetland_soils=has_wetland_soils
2729
- )
2730
- ),
2731
- DEFAULT
2732
- ) if should_run else DEFAULT
2733
-
2734
-
2735
- # --- ASSIGN IPCC MANAGEMENT CATEGORY ---
2736
-
2737
-
2738
- def _check_grassland_ipcc_management_category(
2739
- *, key: IpccManagementCategory, land_cover_nodes: list[dict], **_
2740
- ) -> bool:
2741
- """
2742
- Check if the land cover nodes match the target conditions for a grassland IpccManagementCategory.
2743
-
2744
- Parameters
2745
- ----------
2746
- key : IpccManagementCategory
2747
- The IPCC management category to check.
2748
- land_cover_nodes : list[dict]
2749
- List of land cover nodes to be checked.
2750
-
2751
- Returns
2752
- -------
2753
- bool
2754
- `True` if the conditions match the specified management category, `False` otherwise.
2755
- """
2756
- target_term_id = IPCC_MANAGEMENT_CATEGORY_TO_GRASSLAND_MANAGEMENT_TERM_ID.get(key, None)
2757
- return cumulative_nodes_term_match(
2758
- land_cover_nodes,
2759
- target_term_ids=target_term_id,
2760
- cumulative_threshold=MIN_AREA_THRESHOLD
2761
- )
2762
-
2763
-
2764
- def _check_tillage_ipcc_management_category(
2765
- *, key: IpccManagementCategory, tillage_nodes: list[dict], **_
2766
- ) -> bool:
2767
- """
2768
- Check if the tillage nodes match the target conditions for a tillage IpccManagementCategory.
2769
-
2770
- Parameters
2771
- ----------
2772
- key : IpccManagementCategory
2773
- The IPCC management category to check.
2774
- tillage_nodes : list[dict]
2775
- List of tillage nodes to be checked.
2776
-
2777
- Returns
2778
- -------
2779
- bool
2780
- `True` if the conditions match the specified management category, `False` otherwise.
2781
- """
2782
- LOOKUP = LOOKUPS["tillage"]
2783
- target_lookup_values = IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE.get(key, None)
2784
- return cumulative_nodes_lookup_match(
2785
- tillage_nodes,
2786
- lookup=LOOKUP,
2787
- target_lookup_values=target_lookup_values,
2788
- cumulative_threshold=MIN_AREA_THRESHOLD
2789
- )
2790
-
2791
-
2792
- GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE = {
2793
- IpccManagementCategory.SEVERELY_DEGRADED: _check_grassland_ipcc_management_category,
2794
- IpccManagementCategory.IMPROVED_GRASSLAND: _check_grassland_ipcc_management_category,
2795
- IpccManagementCategory.HIGH_INTENSITY_GRAZING: _check_grassland_ipcc_management_category,
2796
- IpccManagementCategory.NOMINALLY_MANAGED: _check_grassland_ipcc_management_category,
2797
- IpccManagementCategory.OTHER: _check_grassland_ipcc_management_category
2798
- }
2799
- """
2800
- Decision tree mapping IPCC management categories to corresponding check functions for grassland.
2801
-
2802
- Key: IpccManagementCategory
2803
- Value: Corresponding function for checking the match of the given management category based on land cover nodes.
2804
- """
2805
-
2806
- TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE = {
2807
- IpccManagementCategory.FULL_TILLAGE: _check_tillage_ipcc_management_category,
2808
- IpccManagementCategory.REDUCED_TILLAGE: _check_tillage_ipcc_management_category,
2809
- IpccManagementCategory.NO_TILLAGE: _check_tillage_ipcc_management_category
2810
- }
2811
- """
2812
- Decision tree mapping IPCC management categories to corresponding check functions for tillage.
2813
-
2814
- Key: IpccManagementCategory
2815
- Value: Corresponding function for checking the match of the given management category based on tillage nodes.
2816
- """
2817
-
2818
- IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE = {
2819
- IpccLandUseCategory.GRASSLAND: GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE,
2820
- IpccLandUseCategory.ANNUAL_CROPS_WET: TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE,
2821
- IpccLandUseCategory.ANNUAL_CROPS: TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE
2822
- }
2823
- """
2824
- Decision tree mapping IPCC land use categories to corresponding decision trees for management categories.
2825
-
2826
- Key: IpccLandUseCategory
2827
- Value: Corresponding decision tree for IPCC management categories based on land use categories.
2828
- """
2829
-
2830
- IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY = {
2831
- IpccLandUseCategory.GRASSLAND: IpccManagementCategory.NOMINALLY_MANAGED,
2832
- IpccLandUseCategory.ANNUAL_CROPS_WET: IpccManagementCategory.FULL_TILLAGE,
2833
- IpccLandUseCategory.ANNUAL_CROPS: IpccManagementCategory.FULL_TILLAGE
2834
- }
2835
- """
2836
- Mapping of default IPCC management categories for each IPCC land use category.
2837
-
2838
- Key: IpccLandUseCategory
2839
- Value: Default IPCC management category for the given land use category.
2840
- """
2841
-
2842
-
2843
- def _assign_ipcc_management_category(
2844
- management_nodes: list[dict], ipcc_land_use_category: IpccLandUseCategory
2845
- ) -> IpccManagementCategory:
2846
- """
2847
- Assign an IPCC Management Category based on the given management nodes and IPCC Land Use Category.
2848
-
2849
- Parameters
2850
- ----------
2851
- management_nodes : list[dict]
2852
- List of management nodes.
2853
- ipcc_land_use_category : IpccLandUseCategory
2854
- The IPCC Land Use Category.
2855
-
2856
- Returns
2857
- -------
2858
- IpccManagementCategory
2859
- The assigned IPCC Management Category.
2860
- """
2861
- decision_tree = IPCC_LAND_USE_CATEGORY_TO_DECISION_TREE.get(ipcc_land_use_category, {})
2862
- default = IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY.get(
2863
- ipcc_land_use_category, IpccManagementCategory.OTHER
2864
- )
2865
-
2866
- land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
2867
- tillage_nodes = filter_list_term_type(management_nodes, [TermTermType.TILLAGE])
2868
-
2869
- should_run = any([
2870
- decision_tree == GRASSLAND_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE and len(land_cover_nodes) > 0,
2871
- decision_tree == TILLAGE_IPCC_MANAGEMENT_CATEGORY_DECISION_TREE and len(tillage_nodes) > 0
2872
- ])
2873
-
2874
- return next(
2875
- (
2876
- key for key in decision_tree
2877
- if decision_tree[key](
2878
- key=key,
2879
- land_cover_nodes=land_cover_nodes,
2880
- tillage_nodes=tillage_nodes,
2881
- )
2882
- ),
2883
- default
2884
- ) if should_run else default
2885
-
2886
-
2887
- # --- ASSIGN IPCC CARBON INPUT CATEGORY ---
2888
-
2889
-
2890
- GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS = {
2891
- IpccCarbonInputCategory.GRASSLAND_HIGH: 2,
2892
- IpccCarbonInputCategory.GRASSLAND_MEDIUM: 1
2893
- }
2894
- """
2895
- A mapping from IPCC Grassland Carbon Input Categories to the minimum number of improvements required.
2896
-
2897
- Key: IpccCarbonInputCategory
2898
- Value: Minimum number of improvements required for the corresponding Grassland Carbon Input Category.
2899
- """
2900
-
2901
-
2902
- def _check_grassland_ipcc_carbon_input_category(
2903
- *, key: IpccCarbonInputCategory, num_grassland_improvements: int, **_,
2904
- ) -> bool:
2905
- """
2906
- Checks if the given carbon input arguments satisfy the conditions for a specific
2907
- Grassland IPCC Carbon Input Category.
2908
-
2909
- Parameters
2910
- ----------
2911
- key : IpccCarbonInputCategory
2912
- The grassland IPCC Carbon Input Category to check.
2913
- num_grassland_improvements : int
2914
- The number of grassland improvements.
2915
-
2916
- Returns
2917
- -------
2918
- bool
2919
- `True` if the conditions for the specified category are met; otherwise, `False`.
2920
- """
2921
- return num_grassland_improvements >= GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_TO_MIN_NUM_IMPROVEMENTS[key]
2922
-
2923
-
2924
- def _check_cropland_high_with_manure_category(
2925
- *,
2926
- has_animal_manure_used: bool,
2927
- has_bare_fallow: bool,
2928
- has_low_residue_producing_crops: bool,
2929
- has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
2930
- has_residue_removed_or_burnt: bool,
2931
- **_
2932
- ) -> Optional[int]:
2933
- """
2934
- Checks the Cropland High with Manure IPCC Carbon Input Category based on the given carbon input arguments.
2935
-
2936
- Parameters
2937
- ----------
2938
- has_animal_manure_used : bool
2939
- Indicates whether animal manure is used on more than 30% of the site.
2940
- has_bare_fallow : bool
2941
- Indicates whether bare fallow is present on more than 30% of the site.
2942
- has_low_residue_producing_crops : bool
2943
- Indicates whether low residue-producing crops are present on more than 70% of the site.
2944
- has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
2945
- Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
2946
- has_residue_removed_or_burnt : bool
2947
- Indicates whether residues are removed or burnt on more than 30% of the site.
2948
-
2949
- Returns
2950
- -------
2951
- int | none
2952
- The category key if conditions are met; otherwise, `None`.
2953
- """
2954
- conditions = {
2955
- 1: all([
2956
- not has_residue_removed_or_burnt,
2957
- not has_low_residue_producing_crops,
2958
- not has_bare_fallow,
2959
- has_n_fixing_crop_or_inorganic_n_fertiliser_used,
2960
- has_animal_manure_used
2961
- ])
2962
- }
2963
-
2964
- return next(
2965
- (key for key, condition in conditions.items() if condition), None
2966
- )
2967
-
2968
-
2969
- def _check_cropland_high_without_manure_category(
2970
- *,
2971
- has_animal_manure_used: bool,
2972
- has_bare_fallow: bool,
2973
- has_cover_crop: bool,
2974
- has_irrigation: bool,
2975
- has_low_residue_producing_crops: bool,
2976
- has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
2977
- has_organic_fertiliser_or_soil_amendment_used: bool,
2978
- has_practice_increasing_c_input: bool,
2979
- has_residue_removed_or_burnt: bool,
2980
- **_
2981
- ) -> Optional[int]:
2982
- """
2983
- Checks the Cropland High without Manure IPCC Carbon Input Category based on the given carbon input arguments.
2984
-
2985
- Parameters
2986
- ----------
2987
- has_animal_manure_used : bool
2988
- Indicates whether animal manure is used on more than 30% of the site.
2989
- has_bare_fallow : bool
2990
- Indicates whether bare fallow is present on more than 30% of the site.
2991
- has_cover_crop : bool
2992
- Indicates whether cover crops are present on more than 30% of the site.
2993
- has_irrigation : bool
2994
- Indicates whether irrigation is applied to more than 30% of the site.
2995
- has_low_residue_producing_crops : bool
2996
- Indicates whether low residue-producing crops are present on more than 70% of the site.
2997
- has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
2998
- Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
2999
- has_organic_fertiliser_or_soil_amendment_used : bool
3000
- Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
3001
- has_practice_increasing_c_input : bool
3002
- Indicates whether practices increasing carbon input are present on more than 30% of the site.
3003
- has_residue_removed_or_burnt : bool
3004
- Indicates whether residues are removed or burnt on more than 30% of the site.
3005
-
3006
- Returns
3007
- -------
3008
- int | None
3009
- The category key if conditions are met; otherwise, `None`.
3010
- """
3011
- conditions = {
3012
- 1: all([
3013
- not has_residue_removed_or_burnt,
3014
- not has_low_residue_producing_crops,
3015
- not has_bare_fallow,
3016
- has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3017
- any([
3018
- has_irrigation,
3019
- has_practice_increasing_c_input,
3020
- has_cover_crop,
3021
- has_organic_fertiliser_or_soil_amendment_used
3022
- ]),
3023
- not has_animal_manure_used
3024
- ])
3025
- }
3026
-
3027
- return next(
3028
- (key for key, condition in conditions.items() if condition), None
3029
- )
3030
-
3031
-
3032
- def _check_cropland_medium_category(
3033
- *,
3034
- has_animal_manure_used: bool,
3035
- has_bare_fallow: bool,
3036
- has_cover_crop: bool,
3037
- has_irrigation: bool,
3038
- has_low_residue_producing_crops: bool,
3039
- has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
3040
- has_organic_fertiliser_or_soil_amendment_used: bool,
3041
- has_practice_increasing_c_input: bool,
3042
- has_residue_removed_or_burnt: bool,
3043
- **_
3044
- ) -> Optional[int]:
3045
- """
3046
- Checks the Cropland Medium IPCC Carbon Input Category based on the given carbon input arguments.
3047
-
3048
- Parameters
3049
- ----------
3050
- has_animal_manure_used : bool
3051
- Indicates whether animal manure is used on more than 30% of the site.
3052
- has_bare_fallow : bool
3053
- Indicates whether bare fallow is present on more than 30% of the site.
3054
- has_cover_crop : bool
3055
- Indicates whether cover crops are present on more than 30% of the site.
3056
- has_irrigation : bool
3057
- Indicates whether irrigation is applied to more than 30% of the site.
3058
- has_low_residue_producing_crops : bool
3059
- Indicates whether low residue-producing crops are present on more than 70% of the site.
3060
- has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
3061
- Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
3062
- has_organic_fertiliser_or_soil_amendment_used : bool
3063
- Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
3064
- has_practice_increasing_c_input : bool
3065
- Indicates whether practices increasing carbon input are present on more than 30% of the site.
3066
- has_residue_removed_or_burnt : bool
3067
- Indicates whether residues are removed or burnt on more than 30% of the site.
3068
-
3069
- Returns
3070
- -------
3071
- int | None
3072
- The category key if conditions are met; otherwise, `None`.
3073
- """
3074
- conditions = {
3075
- 1: all([
3076
- has_residue_removed_or_burnt,
3077
- has_animal_manure_used
3078
- ]),
3079
- 2: all([
3080
- not has_residue_removed_or_burnt,
3081
- any([
3082
- has_low_residue_producing_crops,
3083
- has_bare_fallow
3084
- ]),
3085
- any([
3086
- has_irrigation,
3087
- has_practice_increasing_c_input,
3088
- has_cover_crop,
3089
- has_organic_fertiliser_or_soil_amendment_used,
3090
- ])
3091
- ]),
3092
- 3: all([
3093
- not has_residue_removed_or_burnt,
3094
- not has_low_residue_producing_crops,
3095
- not has_bare_fallow,
3096
- not has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3097
- any([
3098
- has_irrigation,
3099
- has_practice_increasing_c_input,
3100
- has_cover_crop,
3101
- has_organic_fertiliser_or_soil_amendment_used
3102
- ])
3103
- ]),
3104
- 4: all([
3105
- not has_residue_removed_or_burnt,
3106
- not has_low_residue_producing_crops,
3107
- not has_bare_fallow,
3108
- has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3109
- not has_irrigation,
3110
- not has_organic_fertiliser_or_soil_amendment_used,
3111
- not has_practice_increasing_c_input,
3112
- not has_cover_crop
3113
- ])
3114
- }
3115
-
3116
- return next(
3117
- (key for key, condition in conditions.items() if condition), None
3118
- )
3119
-
3120
-
3121
- def _check_cropland_low_category(
3122
- *,
3123
- has_animal_manure_used: bool,
3124
- has_bare_fallow: bool,
3125
- has_cover_crop: bool,
3126
- has_irrigation: bool,
3127
- has_low_residue_producing_crops: bool,
3128
- has_n_fixing_crop_or_inorganic_n_fertiliser_used: bool,
3129
- has_organic_fertiliser_or_soil_amendment_used: bool,
3130
- has_practice_increasing_c_input: bool,
3131
- has_residue_removed_or_burnt: bool,
3132
- **_
3133
- ) -> Optional[int]:
3134
- """
3135
- Checks the Cropland Low IPCC Carbon Input Category based on the given carbon input arguments.
3136
-
3137
- Parameters
3138
- ----------
3139
- has_animal_manure_used : bool
3140
- Indicates whether animal manure is used on more than 30% of the site.
3141
- has_bare_fallow : bool
3142
- Indicates whether bare fallow is present on more than 30% of the site.
3143
- has_cover_crop : bool
3144
- Indicates whether cover crops are present on more than 30% of the site.
3145
- has_irrigation : bool
3146
- Indicates whether irrigation is applied to more than 30% of the site.
3147
- has_low_residue_producing_crops : bool
3148
- Indicates whether low residue-producing crops are present on more than 70% of the site.
3149
- has_n_fixing_crop_or_inorganic_n_fertiliser_used : bool
3150
- Indicates whether a nitrogen-fixing crop or inorganic nitrogen fertiliser is used on more than 30% of the site.
3151
- has_organic_fertiliser_or_soil_amendment_used : bool
3152
- Indicates whether organic fertiliser or soil amendments are used on more than 30% of the site.
3153
- has_practice_increasing_c_input : bool
3154
- Indicates whether practices increasing carbon input are present on more than 30% of the site.
3155
- has_residue_removed_or_burnt : bool
3156
- Indicates whether residues are removed or burnt on more than 30% of the site.
3157
-
3158
- Returns
3159
- -------
3160
- int | None
3161
- The category key if conditions are met; otherwise, `None`.
3162
- """
3163
- conditions = {
3164
- 1: all([
3165
- has_residue_removed_or_burnt,
3166
- not has_animal_manure_used
3167
- ]),
3168
- 2: all([
3169
- not has_residue_removed_or_burnt,
3170
- any([
3171
- has_low_residue_producing_crops,
3172
- has_bare_fallow
3173
- ]),
3174
- not has_irrigation,
3175
- not has_practice_increasing_c_input,
3176
- not has_cover_crop,
3177
- not has_organic_fertiliser_or_soil_amendment_used
3178
- ]),
3179
- 3: all([
3180
- not has_residue_removed_or_burnt,
3181
- not has_low_residue_producing_crops,
3182
- not has_bare_fallow,
3183
- not has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3184
- not has_irrigation,
3185
- not has_organic_fertiliser_or_soil_amendment_used,
3186
- not has_practice_increasing_c_input,
3187
- not has_cover_crop
3188
- ])
3189
- }
3190
-
3191
- return next(
3192
- (key for key, condition in conditions.items() if condition), None
3193
- )
3194
-
3195
-
3196
- def _get_carbon_input_kwargs(
3197
- management_nodes: list[dict]
3198
- ) -> dict:
3199
- """
3200
- Creates CarbonInputArgs based on the provided list of management nodes.
3201
-
3202
- Parameters
3203
- ----------
3204
- management_nodes : list[dict]
3205
- The list of management nodes.
3206
-
3207
- Returns
3208
- -------
3209
- dict
3210
- The carbon input keyword arguments.
3211
- """
3212
-
3213
- PRACTICE_INCREASING_C_INPUT_LOOKUP = LOOKUPS["landUseManagement"]
3214
- LOW_RESIDUE_PRODUCING_CROP_LOOKUP = LOOKUPS["landCover"][1]
3215
- N_FIXING_CROP_LOOKUP = LOOKUPS["landCover"][2]
3216
-
3217
- # To prevent double counting already explicitly checked practices.
3218
- EXCLUDED_PRACTICE_TERM_IDS = {
3219
- IMPROVED_PASTURE_TERM_ID,
3220
- ANIMAL_MANURE_USED_TERM_ID,
3221
- INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID,
3222
- ORGANIC_FERTILISER_USED_TERM_ID
3223
- }
3224
-
3225
- crop_residue_management_nodes = filter_list_term_type(management_nodes, [TermTermType.CROPRESIDUEMANAGEMENT])
3226
- land_cover_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])
3227
- land_use_management_nodes = filter_list_term_type(management_nodes, [TermTermType.LANDUSEMANAGEMENT])
3228
- water_regime_nodes = filter_list_term_type(management_nodes, [TermTermType.WATERREGIME])
3229
-
3230
- has_animal_manure_used = any(
3231
- get_node_value(node) for node in land_use_management_nodes if node_term_match(node, ANIMAL_MANURE_USED_TERM_ID)
3232
- )
3233
-
3234
- has_bare_fallow = cumulative_nodes_term_match(
3235
- land_cover_nodes,
3236
- target_term_ids=SHORT_BARE_FALLOW_TERM_ID,
3237
- cumulative_threshold=MIN_AREA_THRESHOLD
3238
- )
3239
-
3240
- cover_crop_property_terms = get_cover_crop_property_terms()
3241
- has_cover_crop = cumulative_nodes_match(
3242
- lambda node: any(
3243
- get_node_property(node, term_id, False).get("value", False) for term_id in cover_crop_property_terms
3244
- ),
3245
- land_cover_nodes,
3246
- cumulative_threshold=MIN_AREA_THRESHOLD
3247
- )
3248
-
3249
- has_inorganic_n_fertiliser_used = any(
3250
- get_node_value(node) for node in land_use_management_nodes
3251
- if node_term_match(node, INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID)
3252
- )
3253
-
3254
- has_irrigation = _has_irrigation(water_regime_nodes)
3255
-
3256
- # SUPER_MAJORITY_AREA_THRESHOLD
3257
- has_low_residue_producing_crops = cumulative_nodes_lookup_match(
3258
- land_cover_nodes,
3259
- lookup=LOW_RESIDUE_PRODUCING_CROP_LOOKUP,
3260
- target_lookup_values=True,
3261
- cumulative_threshold=SUPER_MAJORITY_AREA_THRESHOLD
3262
- )
3263
-
3264
- has_n_fixing_crop = cumulative_nodes_lookup_match(
3265
- land_cover_nodes,
3266
- lookup=N_FIXING_CROP_LOOKUP,
3267
- target_lookup_values=True,
3268
- cumulative_threshold=MIN_AREA_THRESHOLD
3269
- )
3270
-
3271
- has_n_fixing_crop_or_inorganic_n_fertiliser_used = has_n_fixing_crop or has_inorganic_n_fertiliser_used
3272
-
3273
- has_organic_fertiliser_or_soil_amendment_used = any(
3274
- get_node_value(node) for node in land_use_management_nodes
3275
- if node_term_match(node, [ORGANIC_FERTILISER_USED_TERM_ID, SOIL_AMENDMENT_USED_TERM_ID])
3276
- )
3277
-
3278
- has_practice_increasing_c_input = cumulative_nodes_match(
3279
- lambda node: (
3280
- node_lookup_match(node, PRACTICE_INCREASING_C_INPUT_LOOKUP, True)
3281
- and not node_term_match(node, EXCLUDED_PRACTICE_TERM_IDS)
3282
- ),
3283
- land_use_management_nodes,
3284
- cumulative_threshold=MIN_AREA_THRESHOLD
3285
- )
3286
-
3287
- has_residue_removed_or_burnt = cumulative_nodes_term_match(
3288
- crop_residue_management_nodes,
3289
- target_term_ids=get_residue_removed_or_burnt_terms(),
3290
- cumulative_threshold=MIN_AREA_THRESHOLD
3291
- )
3292
-
3293
- num_grassland_improvements = [
3294
- has_irrigation,
3295
- has_practice_increasing_c_input,
3296
- has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3297
- has_organic_fertiliser_or_soil_amendment_used
3298
- ].count(True)
3299
-
3300
- return {
3301
- "has_animal_manure_used": has_animal_manure_used,
3302
- "has_bare_fallow": has_bare_fallow,
3303
- "has_cover_crop": has_cover_crop,
3304
- "has_irrigation": has_irrigation,
3305
- "has_low_residue_producing_crops": has_low_residue_producing_crops,
3306
- "has_n_fixing_crop_or_inorganic_n_fertiliser_used": has_n_fixing_crop_or_inorganic_n_fertiliser_used,
3307
- "has_organic_fertiliser_or_soil_amendment_used": has_organic_fertiliser_or_soil_amendment_used,
3308
- "has_practice_increasing_c_input": has_practice_increasing_c_input,
3309
- "has_residue_removed_or_burnt": has_residue_removed_or_burnt,
3310
- "num_grassland_improvements": num_grassland_improvements
3311
- }
3312
-
3313
-
3314
- GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE = {
3315
- IpccCarbonInputCategory.GRASSLAND_HIGH: _check_grassland_ipcc_carbon_input_category,
3316
- IpccCarbonInputCategory.GRASSLAND_MEDIUM: _check_grassland_ipcc_carbon_input_category
3317
- }
3318
- """
3319
- A decision tree for assigning IPCC Carbon Input Categories to Grassland based on the number of improvements.
3320
-
3321
- Key: IpccCarbonInputCategory
3322
- Value: Corresponding function to check if the given conditions are met for the category.
3323
- """
3324
-
3325
- CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE = {
3326
- IpccCarbonInputCategory.CROPLAND_HIGH_WITH_MANURE: _check_cropland_high_with_manure_category,
3327
- IpccCarbonInputCategory.CROPLAND_HIGH_WITHOUT_MANURE: _check_cropland_high_without_manure_category,
3328
- IpccCarbonInputCategory.CROPLAND_MEDIUM: _check_cropland_medium_category,
3329
- IpccCarbonInputCategory.CROPLAND_LOW: _check_cropland_low_category
3330
- }
3331
- """
3332
- A decision tree for assigning IPCC Carbon Input Categories to Cropland based on specific conditions.
3333
-
3334
- Key: IpccCarbonInputCategory
3335
- Value: Corresponding function to check if the given conditions are met for the category.
3336
- """
3337
-
3338
- DECISION_TREE_FROM_IPCC_MANAGEMENT_CATEGORY = {
3339
- IpccManagementCategory.IMPROVED_GRASSLAND: GRASSLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE,
3340
- IpccManagementCategory.FULL_TILLAGE: CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE,
3341
- IpccManagementCategory.REDUCED_TILLAGE: CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE,
3342
- IpccManagementCategory.NO_TILLAGE: CROPLAND_IPCC_CARBON_INPUT_CATEGORY_DECISION_TREE
3343
- }
3344
- """
3345
- A decision tree mapping IPCC Management Categories to respective Carbon Input Category decision trees.
3346
-
3347
- Key: IpccManagementCategory
3348
- Value: Decision tree for Carbon Input Categories corresponding to the management category.
3349
- """
3350
-
3351
- DEFAULT_CARBON_INPUT_CATEGORY = {
3352
- IpccManagementCategory.IMPROVED_GRASSLAND: IpccCarbonInputCategory.GRASSLAND_MEDIUM,
3353
- IpccManagementCategory.FULL_TILLAGE: IpccCarbonInputCategory.CROPLAND_LOW,
3354
- IpccManagementCategory.REDUCED_TILLAGE: IpccCarbonInputCategory.CROPLAND_LOW,
3355
- IpccManagementCategory.NO_TILLAGE: IpccCarbonInputCategory.CROPLAND_LOW
3356
- }
3357
- """
3358
- A mapping from IPCC Management Categories to default Carbon Input Categories.
3359
-
3360
- Key: IpccManagementCategory
3361
- Value: Default Carbon Input Category for the corresponding Management Category.
3362
- """
3363
-
3364
-
3365
- def _assign_ipcc_carbon_input_category(
3366
- management_nodes: list[dict],
3367
- ipcc_management_category: IpccManagementCategory
3368
- ) -> IpccCarbonInputCategory:
3369
- """
3370
- Assigns an IPCC Carbon Input Category based on the provided management nodes and IPCC Management Category.
3371
-
3372
- Parameters
3373
- ----------
3374
- management_nodes : list[dict]
3375
- List of management nodes containing information about land management practices.
3376
- ipcc_management_category : IpccManagementCategory
3377
- IPCC Management Category for which the Carbon Input Category needs to be assigned.
3378
-
3379
- Returns
3380
- -------
3381
- IpccCarbonInputCategory
3382
- Assigned IPCC Carbon Input Category.
3383
- """
3384
- decision_tree = DECISION_TREE_FROM_IPCC_MANAGEMENT_CATEGORY.get(ipcc_management_category, {})
3385
- default = DEFAULT_CARBON_INPUT_CATEGORY.get(ipcc_management_category, IpccCarbonInputCategory.OTHER)
3386
-
3387
- should_run = len(management_nodes) > 0
3388
-
3389
- return next(
3390
- (key for key in decision_tree if decision_tree[key](
3391
- key=key,
3392
- **_get_carbon_input_kwargs(management_nodes)
3393
- )),
3394
- default
3395
- ) if should_run else default
3396
-
3397
-
3398
- # --- TIER 1 SOC MODEL ---
3399
-
3400
-
3401
- def _run_tier_1(
3402
- inventory: dict,
3403
- *,
3404
- eco_climate_zone: int,
3405
- soc_ref: float,
3406
- **_
3407
- ) -> list[dict]:
3408
- """
3409
- Run the IPCC (2019) Tier 1 methodology for calculating SOC stocks (in kg C ha-1) for each year in the inventory
3410
- and wrap each of the calculated values in Hestia measurement nodes. To avoid any errors, the `inventory` parameter
3411
- must be pre-validated by the `should_run` function.
3412
-
3413
- See [IPCC (2019) Vol. 4, Ch. 2](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html) for more information.
3414
-
3415
- The inventory should be in the following shape:
3416
- ```
3417
- {
3418
- year (int): {
3419
- _InventoryKey.SHOULD_RUN_TIER_1: bool,
3420
- _InventoryKey.LU_CATEGORY: IpccLandUseCategory,
3421
- _InventoryKey.MG_CATEGORY: IpccManagementCategory,
3422
- _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory
3423
- },
3424
- ...
3425
- }
3426
- ```
3427
-
3428
- Parameters
3429
- ----------
3430
- inventory : dict
3431
- The inventory built by the `_should_run` function.
3432
- eco_climate_zone : int
3433
- The eco-climate zone identifier for the site corresponding to a row in the
3434
- [ecoClimateZone](https://gitlab.com/hestia-earth/hestia-glossary/-/blob/develop/Measurements/ecoClimateZone-lookup.csv)
3435
- lookup table.
3436
- ipcc_soil_category : IpccSoilCategory
3437
- The reference condition SOC stock in the 0-30cm depth interval, kg C ha-1.
3438
-
3439
- Returns
3440
- -------
3441
- list[dict]
3442
- A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
3443
- """
3444
-
3445
- valid_inventory = {
3446
- year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_1)
3447
- }
3448
-
3449
- timestamps = [year for year in valid_inventory.keys()]
3450
- ipcc_land_use_categories = [group[_InventoryKey.LU_CATEGORY] for group in valid_inventory.values()]
3451
- ipcc_management_categories = [group[_InventoryKey.MG_CATEGORY] for group in valid_inventory.values()]
3452
- ipcc_carbon_input_categories = [group[_InventoryKey.CI_CATEGORY] for group in valid_inventory.values()]
3453
-
3454
- iterated_timestamps, iterated_soc_equilibriums = _run_soc_equilibriums(
3455
- timestamps,
3456
- ipcc_land_use_categories,
3457
- ipcc_management_categories,
3458
- ipcc_carbon_input_categories,
3459
- eco_climate_zone,
3460
- soc_ref
3461
- )
3462
-
3463
- soc_stocks = _calc_tier_1_soc_stocks(iterated_timestamps, iterated_soc_equilibriums)
3464
-
3465
- return [
3466
- _measurement(
3467
- year,
3468
- soc_stock,
3469
- MeasurementMethodClassification.TIER_1_MODEL.value
3470
- ) for year, soc_stock in zip(
3471
- iterated_timestamps,
3472
- soc_stocks
3473
- )
3474
- ]
3475
-
3476
-
3477
- # --- SHOULD RUN ---
3478
-
3479
-
3480
- def _should_run(site: dict) -> tuple[bool, dict]:
3481
- """
3482
- Extract data from site & related cycles, pre-process data and determine whether there is sufficient data to run the
3483
- tier 1 and/or tier 2 model.
3484
-
3485
- The inventory dict should be in the following shape:
3486
- ```
3487
- {
3488
- year (int): {
3489
- _InventoryKey.SHOULD_RUN_TIER_2: bool,
3490
- _InventoryKey.TEMP_MONTHLY: list[float],
3491
- _InventoryKey.PRECIP_MONTHLY: list[float],
3492
- _InventoryKey.PET_MONTHLY: list[float],
3493
- _InventoryKey.IRRIGATED_MONTHLY: list[bool]
3494
- _InventoryKey.CARBON_INPUT: float,
3495
- _InventoryKey.N_CONTENT: float,
3496
- _InventoryKey.TILLAGE_CATEGORY: IpccManagementCategory,
3497
- _InventoryKey.SAND_CONTENT: float,
3498
- _InventoryKey.SHOULD_RUN_TIER_1: bool,
3499
- _InventoryKey.LU_CATEGORY: IpccLandUseCategory,
3500
- _InventoryKey.MG_CATEGORY: IpccManagementCategory,
3501
- _InventoryKey.CI_CATEGORY: IpccCarbonInputCategory
3502
- },
3503
- ...
3504
- }
3505
- ```
3506
-
3507
- The kwargs dict should be in the following shape:
3508
- ```
3509
- {
3510
- "run_with_irrigation": bool,
3511
- "eco_climate_zone": int,
3512
- "ipcc_soil_category": IpccSoilCategory,
3513
- "soc_ref": float
3514
- }
3515
- ```
3516
- """
3517
- site_type = site.get("siteType", "")
3518
- management_nodes = site.get("management", [])
3519
- measurement_nodes = site.get("measurements", [])
3520
- cycles = related_cycles(site)
3521
-
3522
- has_management = len(management_nodes) > 0
3523
- has_measurements = len(measurement_nodes) > 0
3524
- has_related_cycles = len(cycles) > 0
3525
- has_functional_unit_1_ha = all(cycle.get("functionalUnit") in VALID_FUNCTIONAL_UNITS_TIER_2 for cycle in cycles)
3526
-
3527
- should_build_inventory_tier_1 = all([
3528
- site_type in VALID_SITE_TYPES_TIER_1,
3529
- has_management,
3530
- has_measurements
3531
- ])
3532
-
3533
- should_build_inventory_tier_2 = all([
3534
- site_type in VALID_SITE_TYPES_TIER_2,
3535
- has_related_cycles,
3536
- check_cycle_site_ids_identical(cycles),
3537
- has_functional_unit_1_ha
3538
- ])
3539
-
3540
- inventory_tier_1, kwargs_tier_1 = (
3541
- _build_inventory_tier_1(site_type, management_nodes, measurement_nodes)
3542
- if should_build_inventory_tier_1 else ({}, {})
3543
- )
3544
-
3545
- inventory_tier_2, kwargs_tier_2 = (
3546
- _build_inventory_tier_2(cycles, measurement_nodes)
3547
- if should_build_inventory_tier_2 else ({}, {})
3548
- )
3549
-
3550
- inventory = dict(sorted(merge(inventory_tier_1, inventory_tier_2).items()))
3551
- kwargs = kwargs_tier_1 | kwargs_tier_2
3552
-
3553
- should_run_tier_1 = _should_run_tier_1(inventory, **kwargs) if should_build_inventory_tier_1 else False
3554
- should_run_tier_2 = _should_run_tier_2(inventory, **kwargs) if should_build_inventory_tier_2 else False
3555
-
3556
- logRequirements(
3557
- site, model=MODEL, term=TERM_ID,
3558
- should_build_inventory_tier_1=should_build_inventory_tier_1,
3559
- should_build_inventory_tier_2=should_build_inventory_tier_2,
3560
- should_run_tier_1=should_run_tier_1,
3561
- should_run_tier_2=should_run_tier_2,
3562
- site_type=site_type,
3563
- has_management=has_management,
3564
- has_measurements=has_measurements,
3565
- has_related_cycles=has_related_cycles,
3566
- is_unit_hectare=has_functional_unit_1_ha,
3567
- **kwargs,
3568
- inventory=_log_inventory(inventory)
3569
- )
3570
-
3571
- should_run = should_run_tier_1 or should_run_tier_2
3572
- logShouldRun(site, MODEL, TERM_ID, should_run)
3573
-
3574
- return should_run_tier_1, should_run_tier_2, inventory, kwargs
3575
-
3576
-
3577
- def _should_run_tier_1(
3578
- inventory: dict,
3579
- *,
3580
- eco_climate_zone: int = None,
3581
- soc_ref: float = None,
3582
- **_
3583
- ) -> bool:
3584
- """
3585
- Determines whether there is sufficient data in the inventory and keyword args to run the tier 1 model.
3586
- """
3587
- return all([
3588
- eco_climate_zone and eco_climate_zone not in EXCLUDED_ECO_CLIMATE_ZONES_TIER_1,
3589
- soc_ref and soc_ref > 0,
3590
- any(year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_1))
3591
- ])
3592
-
3593
-
3594
- def _should_run_tier_2(
3595
- inventory: dict,
3596
- *,
3597
- sand_content: float = None,
3598
- **_
3599
- ) -> bool:
3600
- """
3601
- Determines whether there is sufficient data in the inventory and keyword args to run the tier 2 model.
3602
- """
3603
- valid_years = [year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN_TIER_2)]
3604
- return all([
3605
- len(valid_years) >= MIN_RUN_IN_PERIOD,
3606
- check_consecutive(valid_years),
3607
- any(inventory.get(year).get(_InventoryKey.SAND_CONTENT) for year in valid_years) or sand_content
3608
- ])
3609
-
3610
-
3611
- # --- LOGGING ---
3612
-
3613
-
3614
- def _log_inventory(inventory: dict) -> str:
3615
- """
3616
- Format the inventory data as a table for logging.
3617
- """
3618
- log_table = log_as_table(
3619
- {
3620
- "year": year,
3621
- "should-run-tier-1": group.get(_InventoryKey.SHOULD_RUN_TIER_1, False),
3622
- "should-run-tier-2": group.get(_InventoryKey.SHOULD_RUN_TIER_2, False),
3623
- "ipcc-land-use-category": (
3624
- group.get(_InventoryKey.LU_CATEGORY).value if group.get(_InventoryKey.LU_CATEGORY) else None
3625
- ),
3626
- "ipcc-management-category": (
3627
- group.get(_InventoryKey.MG_CATEGORY).value if group.get(_InventoryKey.MG_CATEGORY) else None
3628
- ),
3629
- "ipcc-carbon-input-category": (
3630
- group.get(_InventoryKey.CI_CATEGORY).value if group.get(_InventoryKey.CI_CATEGORY) else None
3631
- ),
3632
- "temperature-monthly": (
3633
- " ".join(f"{val:.1f}" for val in group.get(_InventoryKey.TEMP_MONTHLY, []))
3634
- if group.get(_InventoryKey.TEMP_MONTHLY) else None
3635
- ),
3636
- "precipitation-monthly": (
3637
- " ".join(f"{val:.1f}" for val in group.get(_InventoryKey.PRECIP_MONTHLY, []))
3638
- if group.get(_InventoryKey.PRECIP_MONTHLY) else None
3639
- ),
3640
- "pet-monthly": (
3641
- " ".join(f"{val:.1f}" for val in group.get(_InventoryKey.PET_MONTHLY, []))
3642
- if group.get(_InventoryKey.PET_MONTHLY) else None
3643
- ),
3644
- "irrigated-monthly": (
3645
- " ".join(str(val) for val in group.get(_InventoryKey.IRRIGATED_MONTHLY, []))
3646
- if group.get(_InventoryKey.IRRIGATED_MONTHLY) else None
3647
- ),
3648
- "sand-content": group.get(_InventoryKey.SAND_CONTENT, None),
3649
- "carbon-input": group.get(_InventoryKey.CARBON_INPUT, None),
3650
- "n-content": group.get(_InventoryKey.N_CONTENT, None),
3651
- "lignin-content": group.get(_InventoryKey.LIGNIN_CONTENT, None),
3652
- "ipcc-tillage-category": (
3653
- group.get(_InventoryKey.TILLAGE_CATEGORY).value if group.get(_InventoryKey.TILLAGE_CATEGORY) else None
3654
- ),
3655
- "is-paddy-rice": group.get(_InventoryKey.IS_PADDY_RICE, None),
3656
- } for year, group in inventory.items()
3657
- )
3658
-
3659
- return log_table or None
3660
-
3661
-
3662
- # --- TIER 2 BUILD INVENTORY ---
3663
-
3664
-
3665
- def _build_inventory_tier_2(
3666
- cycles: list[dict], measurement_nodes: list[dict]
3667
- ) -> tuple[dict, dict]:
3668
- """
3669
- Builds an annual inventory of data and a dictionary of keyword arguments for the tier 2 model.
3670
-
3671
- TODO: implement long-term average climate data and annual climate data as back ups for monthly data
3672
- """
3673
- grouped_cycles = group_nodes_by_year(cycles)
3674
- grouped_measurements = group_nodes_by_year(measurement_nodes, mode=GroupNodesByYearMode.DATES)
3675
-
3676
- grouped_climate_data = _get_grouped_climate_measurements(grouped_measurements)
3677
- grouped_irrigated_monthly = _get_grouped_irrigated_monthly(grouped_cycles)
3678
- grouped_sand_content_measurements = _get_grouped_sand_content_measurements(grouped_measurements)
3679
- grouped_carbon_input_data = _get_grouped_carbon_input_data(grouped_cycles)
3680
- grouped_tillage_categories = _get_grouped_tillage_categories(grouped_cycles)
3681
- grouped_is_paddy_rice = _get_grouped_is_paddy_rice(grouped_cycles)
3682
-
3683
- grouped_data = merge(
3684
- grouped_climate_data,
3685
- grouped_irrigated_monthly,
3686
- grouped_sand_content_measurements,
3687
- grouped_carbon_input_data,
3688
- grouped_tillage_categories,
3689
- grouped_is_paddy_rice
3690
- )
3691
-
3692
- grouped_should_run = {
3693
- year: {_InventoryKey.SHOULD_RUN_TIER_2: _should_run_inventory_year_tier_2(group)}
3694
- for year, group in grouped_data.items()
3695
- }
3696
-
3697
- inventory = merge(grouped_data, grouped_should_run)
3698
-
3699
- # get a back-up value for sand content if no dated ones are available
3700
- sand_content = get_node_value(find_term_match(
3701
- [m for m in measurement_nodes if m.get("depthUpper") == DEPTH_UPPER and m.get("depthLower") == DEPTH_LOWER],
3702
- SAND_CONTENT_TERM_ID,
3703
- {}
3704
- )) / 100
3705
-
3706
- kwargs = {
3707
- "run_with_irrigation": True,
3708
- "sand_content": sand_content
3709
- }
3710
-
3711
- return inventory, kwargs
3712
-
3713
-
3714
- def _should_run_inventory_year_tier_2(group: dict) -> bool:
3715
- """
3716
- Determines whether there is sufficient data in an inventory year to run the tier 2 model.
3717
-
3718
- 1. Check that the cycle is not for paddy rice.
3719
- 2. Check if monthly data has a value for each calendar month.
3720
- 3. Check if all required keys are present.
3721
-
3722
- Parameters
3723
- ----------
3724
- group : dict
3725
- Dictionary containing information for a specific inventory year.
3726
-
3727
- Returns
3728
- -------
3729
- bool
3730
- True if the inventory year is valid, False otherwise.
3731
- """
3732
- monthly_data_complete = _check_12_months(
3733
- group,
3734
- {
3735
- _InventoryKey.TEMP_MONTHLY,
3736
- _InventoryKey.PRECIP_MONTHLY,
3737
- _InventoryKey.PET_MONTHLY,
3738
- _InventoryKey.IRRIGATED_MONTHLY
3739
- }
3740
- )
3741
-
3742
- carbon_input_data_complete = all([
3743
- group.get(_InventoryKey.CARBON_INPUT, 0) > 0,
3744
- group.get(_InventoryKey.N_CONTENT, 0) > 0,
3745
- group.get(_InventoryKey.LIGNIN_CONTENT, 0) > 0,
3746
- ])
3747
-
3748
- return all([
3749
- not group.get(_InventoryKey.IS_PADDY_RICE),
3750
- monthly_data_complete,
3751
- carbon_input_data_complete,
3752
- all(key in group.keys() for key in REQUIRED_KEYS_TIER_2),
3753
- ])
3754
-
3755
-
3756
- def _get_grouped_climate_measurements(grouped_measurements: dict) -> dict:
3757
- return {
3758
- year: {
3759
- _InventoryKey.TEMP_MONTHLY: non_empty_list(
3760
- find_term_match(measurements, TEMPERATURE_MONTHLY_TERM_ID, {}).get("value", [])
3761
- ),
3762
- _InventoryKey.PRECIP_MONTHLY: non_empty_list(
3763
- find_term_match(measurements, PRECIPITATION_MONTHLY_TERM_ID, {}).get("value", [])
3764
- ),
3765
- _InventoryKey.PET_MONTHLY: non_empty_list(
3766
- find_term_match(measurements, PET_MONTHLY_TERM_ID, {}).get("value", [])
3767
- )
3768
- } for year, measurements in grouped_measurements.items()
3769
- }
3770
-
3771
-
3772
- def _get_grouped_irrigated_monthly(grouped_cycles: dict) -> dict:
3773
- irrigated_terms = get_irrigated_terms()
3774
-
3775
- return {
3776
- year: {
3777
- _InventoryKey.IRRIGATED_MONTHLY: _get_irrigated_monthly(year, cycles, irrigated_terms)
3778
- } for year, cycles in grouped_cycles.items()
3779
- }
3780
-
3781
-
3782
- def _get_irrigated_monthly(year: int, cycles: list[dict], irrigated_terms: list[str]) -> list[bool]:
3783
- # Get practice nodes and add "startDate" and "endDate" from cycle if missing.
3784
- irrigation_nodes = non_empty_list(flatten([
3785
- [
3786
- {
3787
- "startDate": cycle.get("startDate"),
3788
- "endDate": cycle.get("endDate"),
3789
- **node
3790
- } for node in cycle.get("practices", [])
3791
- ] for cycle in cycles
3792
- ]))
3793
-
3794
- grouped_nodes = group_nodes_by_year_and_month(irrigation_nodes)
3795
-
3796
- # For each month (1 - 12) check if irrigation is present.
3797
- return [
3798
- cumulative_nodes_term_match(
3799
- grouped_nodes.get(year, {}).get(month, []),
3800
- target_term_ids=irrigated_terms,
3801
- cumulative_threshold=MIN_AREA_THRESHOLD
3802
- ) for month in range(1, 13)
3803
- ]
3804
-
3805
-
3806
- def _get_grouped_sand_content_measurements(grouped_measurements: dict) -> dict:
3807
- grouped_sand_content_measurements = {
3808
- year: find_term_match(
3809
- [m for m in measurements if m.get("depthUpper") == DEPTH_UPPER and m.get("depthLower") == DEPTH_LOWER],
3810
- SAND_CONTENT_TERM_ID,
3811
- {}
3812
- ) for year, measurements in grouped_measurements.items()
3813
- }
3814
-
3815
- return {
3816
- year: {_InventoryKey.SAND_CONTENT: get_node_value(measurement)/100}
3817
- for year, measurement in grouped_sand_content_measurements.items() if measurement
3818
- }
3819
-
3820
-
3821
- def _get_grouped_carbon_input_data(grouped_cycles: dict) -> dict:
3822
- grouped_carbon_sources = {
3823
- year: _get_carbon_sources_from_cycles(cycle)
3824
- for year, cycle in grouped_cycles.items()
3825
- }
3826
-
3827
- return {
3828
- year: {
3829
- _InventoryKey.CARBON_INPUT: _calc_total_organic_carbon_input(carbon_sources),
3830
- _InventoryKey.N_CONTENT: _calc_average_nitrogen_content_of_organic_carbon_sources(carbon_sources),
3831
- _InventoryKey.LIGNIN_CONTENT: _calc_average_lignin_content_of_organic_carbon_sources(carbon_sources)
3832
- } for year, carbon_sources in grouped_carbon_sources.items()
3833
- }
3834
-
3835
-
3836
- def _get_grouped_tillage_categories(grouped_cycles):
3837
- return {
3838
- year: {
3839
- _InventoryKey.TILLAGE_CATEGORY: _assign_tier_2_ipcc_tillage_management_category(cycles)
3840
- } for year, cycles in grouped_cycles.items()
3841
- }
3842
-
3843
-
3844
- def _get_grouped_is_paddy_rice(grouped_cycles: dict) -> dict:
3845
- return {
3846
- year: {
3847
- _InventoryKey.IS_PADDY_RICE: _check_is_paddy_rice(cycles)
3848
- } for year, cycles in grouped_cycles.items()
3849
- }
3850
-
3851
-
3852
- def _check_is_paddy_rice(cycles: list[dict]) -> bool:
3853
- LOOKUP = LOOKUPS["crop"]
3854
- TARGET_LOOKUP_VALUES = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(
3855
- IpccLandUseCategory.PADDY_RICE_CULTIVATION, None
3856
- )
3857
-
3858
- has_paddy_rice_products = any(cumulative_nodes_lookup_match(
3859
- filter_list_term_type(
3860
- cycle.get("products", []) + cycle.get("practices", []),
3861
- [TermTermType.CROP, TermTermType.FORAGE, TermTermType.LANDCOVER]
3862
- ),
3863
- lookup=LOOKUP,
3864
- target_lookup_values=TARGET_LOOKUP_VALUES,
3865
- cumulative_threshold=MIN_YIELD_THRESHOLD,
3866
- default_node_value=MIN_YIELD_THRESHOLD
3867
- ) for cycle in cycles)
3868
-
3869
- reice_terms = get_upland_rice_crop_terms() + get_upland_rice_land_cover_terms()
3870
- has_upland_rice_products = any(cumulative_nodes_term_match(
3871
- filter_list_term_type(
3872
- cycle.get("products", []) + cycle.get("practices", []),
3873
- [TermTermType.CROP, TermTermType.FORAGE, TermTermType.LANDCOVER]
3874
- ),
3875
- target_term_ids=reice_terms,
3876
- cumulative_threshold=MIN_YIELD_THRESHOLD,
3877
- default_node_value=MIN_YIELD_THRESHOLD
3878
- ) for cycle in cycles)
3879
-
3880
- has_irrigation = any(
3881
- _has_irrigation(filter_list_term_type(cycle.get("practices", []), [TermTermType.WATERREGIME]))
3882
- for cycle in cycles
3883
- )
3884
-
3885
- return has_paddy_rice_products or (has_upland_rice_products and has_irrigation)
3886
-
3887
-
3888
- # --- TIER 1 BUILD INVENTORY ---
3889
-
3890
-
3891
- def _build_inventory_tier_1(
3892
- site_type: str, management_nodes: list[dict], measurement_nodes: list[dict]
3893
- ) -> tuple[dict, dict]:
3894
- """
3895
- Builds an annual inventory of data and a dictionary of keyword arguments for the tier 2 model.
3896
- """
3897
- eco_climate_zone = _get_eco_climate_zone(measurement_nodes)
3898
- ipcc_soil_category = _assign_ipcc_soil_category(measurement_nodes)
3899
- soc_ref = _retrieve_soc_ref(eco_climate_zone, ipcc_soil_category)
3900
- grouped_management = group_nodes_by_year(management_nodes)
3901
-
3902
- # If no `landCover` nodes in `site.management` use `site.siteType` to assign static `IpccLandUseCategory`
3903
- run_with_site_type = len(filter_list_term_type(management_nodes, [TermTermType.LANDCOVER])) == 0
3904
- site_type_ipcc_land_use_category = SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY.get(site_type, IpccLandUseCategory.OTHER)
3905
-
3906
- grouped_management = group_nodes_by_year(management_nodes)
3907
-
3908
- grouped_land_use_categories = {
3909
- year: {
3910
- _InventoryKey.LU_CATEGORY: (
3911
- site_type_ipcc_land_use_category if run_with_site_type
3912
- else _assign_ipcc_land_use_category(nodes, ipcc_soil_category)
3913
- )
3914
- } for year, nodes in grouped_management.items()
3915
- }
3916
-
3917
- grouped_management_categories = {
3918
- year: {
3919
- _InventoryKey.MG_CATEGORY: _assign_ipcc_management_category(
3920
- nodes,
3921
- grouped_land_use_categories[year][_InventoryKey.LU_CATEGORY]
3922
- )
3923
- } for year, nodes in grouped_management.items()
3924
- }
3925
-
3926
- grouped_carbon_input_categories = {
3927
- year: {
3928
- _InventoryKey.CI_CATEGORY: _assign_ipcc_carbon_input_category(
3929
- nodes,
3930
- grouped_management_categories[year][_InventoryKey.MG_CATEGORY]
3931
- )
3932
- } for year, nodes in grouped_management.items()
3933
- }
3934
-
3935
- grouped_data = merge(
3936
- grouped_land_use_categories,
3937
- grouped_management_categories,
3938
- grouped_carbon_input_categories
3939
- )
3940
-
3941
- grouped_should_run = {
3942
- year: {_InventoryKey.SHOULD_RUN_TIER_1: _should_run_inventory_year_tier_1(group)}
3943
- for year, group in grouped_data.items()
3944
- }
3945
-
3946
- inventory = merge(grouped_data, grouped_should_run)
3947
- kwargs = {
3948
- "eco_climate_zone": eco_climate_zone,
3949
- "ipcc_soil_category": ipcc_soil_category,
3950
- "run_with_site_type": run_with_site_type,
3951
- "soc_ref": soc_ref
3952
- }
3953
-
3954
- return inventory, kwargs
3955
-
3956
-
3957
- def _should_run_inventory_year_tier_1(group: dict) -> bool:
3958
- """
3959
- Determines whether there is sufficient data in an inventory year to run the tier 1 model.
3960
-
3961
- 1. Check if the land use category is not "OTHER"
3962
- 2. Check if all required keys are present.
3963
-
3964
- Parameters
3965
- ----------
3966
- group : dict
3967
- Dictionary containing information for a specific inventory year.
3968
-
3969
- Returns
3970
- -------
3971
- bool
3972
- True if the inventory year is valid, False otherwise.
3973
- """
3974
- return all([
3975
- group.get(_InventoryKey.LU_CATEGORY) != IpccLandUseCategory.OTHER,
3976
- all(key in group.keys() for key in REQUIRED_KEYS_TIER_1),
3977
- ])
3978
-
3979
-
3980
- # --- RUN ---
3981
-
3982
-
3983
- def run(site: dict) -> list[dict]:
3984
- """
3985
- Check which Tiers of IPCC SOC model to run, run it and return the formatted output.
3986
-
3987
- Parameters
3988
- ----------
3989
- site : dict
3990
- A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
3991
-
3992
- Returns
3993
- -------
3994
- list[dict]
3995
- A list of Hestia `Measurement` nodes containing the calculated SOC stocks and additional relevant data.
3996
- """
3997
- should_run_tier_1, should_run_tier_2, inventory, kwargs = _should_run(site)
3998
- return (
3999
- _run_tier_2(inventory, **kwargs) if should_run_tier_2
4000
- else _run_tier_1(inventory, **kwargs) if should_run_tier_1
4001
- else []
4002
- )
235
+ def _run_method(method: ModuleType, should_run: bool, inventory: dict, kwargs: dict, **_) -> list[dict]:
236
+ return method.run(inventory, **kwargs, iterations=ITERATIONS) if should_run else []