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
@@ -0,0 +1,1630 @@
1
+ """
2
+ The IPCC Tier 2 methodology for estimating soil organic carbon stock changes in the 0 - 30cm depth interval due to
3
+ management changes.
4
+
5
+ More information on this model, including data requirements **and** recommendations, and examples can be found in the
6
+ [Hestia SOC wiki](https://gitlab.com/hestia-earth/hestia-engine-models/-/wikis/Soil-organic-carbon-modelling).
7
+
8
+ Source: [IPCC 2019, Vol. 4, Chapter 5](https://www.ipcc-nggip.iges.or.jp/public/2019rf/vol4.html).
9
+ """
10
+
11
+ from enum import Enum
12
+ from numpy import array, empty, exp, minimum, random, where, vstack
13
+ from numpy.typing import NDArray
14
+ from pydash.objects import merge
15
+ from typing import Any, Callable, Union
16
+
17
+ from hestia_earth.schema import (
18
+ CycleFunctionalUnit, MeasurementMethodClassification, SiteSiteType, TermTermType
19
+ )
20
+ from hestia_earth.utils.model import find_term_match, filter_list_term_type
21
+ from hestia_earth.utils.tools import flatten, list_sum, non_empty_list
22
+
23
+ from hestia_earth.models.utils.array_builders import (
24
+ avg_run_in_columnwise, gen_seed, grouped_avg, repeat_1d_array_as_columns
25
+ )
26
+ from hestia_earth.models.utils.blank_node import (
27
+ cumulative_nodes_lookup_match, cumulative_nodes_term_match, get_node_value, group_nodes_by_year,
28
+ group_nodes_by_year_and_month, GroupNodesByYearMode, node_term_match
29
+ )
30
+ from hestia_earth.models.utils.cycle import check_cycle_site_ids_identical
31
+ from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
32
+ from hestia_earth.models.utils.measurement import _new_measurement
33
+ from hestia_earth.models.utils.property import get_node_property
34
+ from hestia_earth.models.utils.site import related_cycles
35
+
36
+ from .organicCarbonPerHa_utils import (
37
+ CarbonSource, check_consecutive, DEPTH_LOWER, DEPTH_UPPER, check_irrigation,
38
+ get_crop_residue_inc_or_left_terms_with_cache,
39
+ get_upland_rice_crop_terms_with_cache,
40
+ get_upland_rice_land_cover_terms_with_cache,
41
+ IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE, IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE,
42
+ IpccLandUseCategory, IpccManagementCategory, MIN_AREA_THRESHOLD, MIN_YIELD_THRESHOLD, sample_constant,
43
+ sample_plus_minus_uncertainty, sample_truncated_normal, STATS_DEFINITION
44
+ )
45
+
46
+ _LOOKUPS = {
47
+ "crop": "IPCC_LAND_USE_CATEGORY",
48
+ "tillage": "IPCC_TILLAGE_MANAGEMENT_CATEGORY"
49
+ }
50
+
51
+ _TERM_ID = 'organicCarbonPerHa'
52
+ _METHOD_CLASSIFICATION = MeasurementMethodClassification.TIER_2_MODEL.value
53
+
54
+ _RUN_IN_PERIOD = 5
55
+
56
+ _SAND_CONTENT_TERM_ID = "sandContent"
57
+ _NUMBER_OF_TILLAGES_TERM_ID = "numberOfTillages"
58
+ _TEMPERATURE_MONTHLY_TERM_ID = "temperatureMonthly"
59
+ _PRECIPITATION_MONTHLY_TERM_ID = "precipitationMonthly"
60
+ _PET_MONTHLY_TERM_ID = "potentialEvapotranspirationMonthly"
61
+ _CARBON_CONTENT_TERM_ID = "carbonContent"
62
+ _NITROGEN_CONTENT_TERM_ID = "nitrogenContent"
63
+ _LIGNIN_CONTENT_TERM_ID = "ligninContent"
64
+
65
+ _CARBON_INPUT_PROPERTY_TERM_IDS = [
66
+ _CARBON_CONTENT_TERM_ID,
67
+ _NITROGEN_CONTENT_TERM_ID,
68
+ _LIGNIN_CONTENT_TERM_ID
69
+ ]
70
+
71
+ _CARBON_SOURCE_TERM_TYPES = [
72
+ TermTermType.ORGANICFERTILISER.value,
73
+ TermTermType.SOILAMENDMENT.value
74
+ ]
75
+
76
+ _VALID_SITE_TYPES = [
77
+ SiteSiteType.CROPLAND.value
78
+ ]
79
+
80
+ _VALID_FUNCTIONAL_UNITS = [
81
+ CycleFunctionalUnit._1_HA.value
82
+ ]
83
+
84
+
85
+ def _measurement(
86
+ timestamps: list[int],
87
+ descriptive_stats_dict: dict
88
+ ) -> dict:
89
+ """
90
+ Build a Hestia `Measurement` node to contain a value and descriptive statistics calculated by the models.
91
+
92
+ The `descriptive_stats_dict` parameter should include the following keys and values from the
93
+ [Measurement](https://www-staging.hestia.earth/schema/Measurement) schema:
94
+ ```
95
+ {
96
+ "value": list[float],
97
+ "sd": list[float],
98
+ "min": list[float],
99
+ "max": list[float],
100
+ "statsDefinition": str,
101
+ "observations": list[int]
102
+ }
103
+ ```
104
+
105
+ Parameters
106
+ ----------
107
+ timestamps : list[int]
108
+ A list of calendar years associated to the calculated SOC stocks.
109
+ descriptive_stats_dict : dict
110
+ A dict containing the descriptive statistics data that should be added to the node.
111
+
112
+ Returns
113
+ -------
114
+ dict
115
+ A valid Hestia `Measurement` node, see: https://www.hestia.earth/schema/Measurement.
116
+ """
117
+ measurement = _new_measurement(_TERM_ID) | descriptive_stats_dict
118
+ measurement["dates"] = [f"{year}-12-31" for year in timestamps]
119
+ measurement["depthUpper"] = DEPTH_UPPER
120
+ measurement["depthLower"] = DEPTH_LOWER
121
+ measurement["methodClassification"] = _METHOD_CLASSIFICATION
122
+ return measurement
123
+
124
+
125
+ class _InventoryKey(Enum):
126
+ """
127
+ Enum representing the inner keys of the annual inventory is constructed from site and cycle data.
128
+ """
129
+ TEMP_MONTHLY = 'temperature-monthly'
130
+ PRECIP_MONTHLY = 'precipitation-monthly'
131
+ PET_MONTHLY = 'pet-monthly'
132
+ IRRIGATED_MONTHLY = 'irrigated-monthly'
133
+ CARBON_INPUT = 'carbon-input'
134
+ N_CONTENT = 'nitrogen-content'
135
+ LIGNIN_CONTENT = 'lignin-content'
136
+ TILLAGE_CATEGORY = 'ipcc-tillage-category'
137
+ SAND_CONTENT = 'sand-content'
138
+ IS_PADDY_RICE = 'is-paddy-rice'
139
+ SHOULD_RUN = 'should-run-tier-2'
140
+
141
+
142
+ _REQUIRED_KEYS = {
143
+ _InventoryKey.TEMP_MONTHLY,
144
+ _InventoryKey.PRECIP_MONTHLY,
145
+ _InventoryKey.PET_MONTHLY,
146
+ _InventoryKey.CARBON_INPUT,
147
+ _InventoryKey.N_CONTENT,
148
+ _InventoryKey.LIGNIN_CONTENT,
149
+ _InventoryKey.TILLAGE_CATEGORY,
150
+ _InventoryKey.IS_PADDY_RICE
151
+ }
152
+ """
153
+ The `_InventoryKey`s that must have valid values for an inventory year to be included in the model.
154
+ """
155
+
156
+
157
+ class _Parameter(Enum):
158
+ """
159
+ The default Tier 2 model parameters provided in the IPCC (2019) report.
160
+ """
161
+ ACTIVE_DECAY_FACTOR = {
162
+ "value": 7.4,
163
+ "min": 7.4,
164
+ "max": 7.4,
165
+ "sd": 0
166
+ }
167
+ SLOW_DECAY_FACTOR = {
168
+ "value": 0.209,
169
+ "min": 0.058,
170
+ "max": 0.3,
171
+ "sd": 0.566
172
+ }
173
+ PASSIVE_DECAY_FACTOR = {
174
+ "value": 0.00689,
175
+ "min": 0.005,
176
+ "max": 0.01,
177
+ "sd": 0.00125
178
+ }
179
+ F_1 = {
180
+ "value": 0.378,
181
+ "min": 0.01,
182
+ "max": 0.8,
183
+ "sd": 0.0719
184
+ }
185
+ F_2_FULL_TILLAGE = {
186
+ "value": 0.455,
187
+ # No stats available in IPCC excel document.
188
+ }
189
+ F_2_REDUCED_TILLAGE = {
190
+ "value": 0.477
191
+ # No stats available in IPCC excel document.
192
+ }
193
+ F_2_NO_TILLAGE = {
194
+ "value": 0.5
195
+ # No stats available in IPCC excel document.
196
+ }
197
+ F_2_UNKNOWN_TILLAGE = {
198
+ "value": 0.368,
199
+ "min": 0.007,
200
+ "max": 0.5,
201
+ "sd": 0.0998
202
+ }
203
+ F_3 = {
204
+ "value": 0.455,
205
+ "min": 0.1,
206
+ "max": 0.8,
207
+ "sd": 0.201
208
+ }
209
+ F_5 = {
210
+ "value": 0.0855,
211
+ "min": 0.037,
212
+ "max": 0.1,
213
+ "sd": 0.0122
214
+ }
215
+ F_6 = {
216
+ "value": 0.0504,
217
+ "min": 0.02,
218
+ "max": 0.19,
219
+ "sd": 0.0280
220
+ }
221
+ F_7 = {
222
+ "value": 0.42,
223
+ "min": 0.42,
224
+ "max": 0.42,
225
+ "sd": 0
226
+ }
227
+ F_8 = {
228
+ "value": 0.45,
229
+ "min": 0.45,
230
+ "max": 0.45,
231
+ "sd": 0
232
+ }
233
+ TILLAGE_FACTOR_FULL_TILLAGE = {
234
+ "value": 3.036,
235
+ "min": 1.4,
236
+ "max": 4.0,
237
+ "sd": 0.579
238
+ }
239
+ TILLAGE_FACTOR_REDUCED_TILLAGE = {
240
+ "value": 2.075,
241
+ "min": 1.0,
242
+ "max": 3.0,
243
+ "sd": 0.569
244
+ }
245
+ TILLAGE_FACTOR_NO_TILLAGE = {
246
+ "value": 1,
247
+ "min": 1,
248
+ "max": 1,
249
+ "sd": 0
250
+ }
251
+ MAXIMUM_TEMPERATURE = {
252
+ "value": 45,
253
+ "min": 45,
254
+ "max": 45,
255
+ "sd": 0
256
+ }
257
+ OPTIMUM_TEMPERATURE = {
258
+ "value": 33.69,
259
+ "min": 30.7,
260
+ "max": 35.34,
261
+ "sd": 0.66
262
+ }
263
+ WATER_FACTOR_SLOPE = {
264
+ "value": 1.331,
265
+ "min": 0.8,
266
+ "max": 2.0,
267
+ "sd": 0.386
268
+ }
269
+ DEFAULT_CARBON_CONTENT = {
270
+ "value": 0.42
271
+ # No stats provided in IPCC report.
272
+ }
273
+ DEFAULT_NITROGEN_CONTENT = {
274
+ "value": 0.0083,
275
+ "uncertainty": 75
276
+ }
277
+ DEFAULT_LIGNIN_CONTENT = {
278
+ "value": 0.073,
279
+ "uncertainty": 50
280
+ }
281
+
282
+
283
+ _PARAMETER_TO_SAMPLE_FUNCTION = {
284
+ _Parameter.ACTIVE_DECAY_FACTOR: sample_constant,
285
+ _Parameter.F_2_FULL_TILLAGE: sample_constant,
286
+ _Parameter.F_2_REDUCED_TILLAGE: sample_constant,
287
+ _Parameter.F_2_NO_TILLAGE: sample_constant,
288
+ _Parameter.F_7: sample_constant,
289
+ _Parameter.F_8: sample_constant,
290
+ _Parameter.TILLAGE_FACTOR_NO_TILLAGE: sample_constant,
291
+ _Parameter.MAXIMUM_TEMPERATURE: sample_constant,
292
+ _Parameter.DEFAULT_CARBON_CONTENT: sample_constant,
293
+ _Parameter.DEFAULT_NITROGEN_CONTENT: sample_plus_minus_uncertainty,
294
+ _Parameter.DEFAULT_LIGNIN_CONTENT: sample_plus_minus_uncertainty
295
+ }
296
+ _DEFAULT_SAMPLE_FUNCTION = sample_truncated_normal
297
+
298
+
299
+ def _sample_parameter(
300
+ iterations: int,
301
+ parameter: _Parameter,
302
+ seed: Union[int, random.Generator, None] = None
303
+ ) -> NDArray:
304
+ """
305
+ Sample a model `_Parameter` using the function specified in `_PARAMETER_TO_SAMPLE_FUNCTION` or
306
+ `_DEFAULT_SAMPLE_FUNCTION`.
307
+
308
+ Parameters
309
+ ----------
310
+ iterations : int
311
+ The number of samples to be taken.
312
+ parameter : _Parameter
313
+ The model parameter to be sampled.
314
+ seed : int | Generator | None, optional
315
+ A seed to initialize the BitGenerator. If passed a Generator, it will be returned unaltered. If `None`, then
316
+ fresh, unpredictable entropy will be pulled from the OS.
317
+
318
+ Returns
319
+ -------
320
+ NDArray
321
+ A numpy array with shape `(1, iterations)`. All columns contain different sample values.
322
+ """
323
+ kwargs = parameter.value
324
+ func = _get_sample_func(parameter)
325
+ return func(iterations=iterations, seed=seed, **kwargs)
326
+
327
+
328
+ def _get_sample_func(parameter: _Parameter) -> Callable:
329
+ """Extracted into method to allow for mocking of sample function."""
330
+ return _PARAMETER_TO_SAMPLE_FUNCTION.get(parameter, _DEFAULT_SAMPLE_FUNCTION)
331
+
332
+
333
+ # --- TIER 2 MODEL ---
334
+
335
+
336
+ def should_run(site: dict) -> tuple[bool, dict, dict]:
337
+ """
338
+ Extract data from site & related cycles, pre-process data and determine whether there is sufficient data to run the
339
+ Tier 2 model.
340
+
341
+ The returned `inventory` should be a dict with the shape:
342
+ ```
343
+ {
344
+ year (int): {
345
+ _InventoryKey.SHOULD_RUN: bool,
346
+ _InventoryKey.TEMP_MONTHLY: list[float],
347
+ _InventoryKey.PRECIP_MONTHLY: list[float],
348
+ _InventoryKey.PET_MONTHLY: list[float],
349
+ _InventoryKey.IRRIGATED_MONTHLY: list[bool]
350
+ _InventoryKey.CARBON_INPUT: float,
351
+ _InventoryKey.N_CONTENT: float,
352
+ _InventoryKey.TILLAGE_CATEGORY: IpccManagementCategory,
353
+ _InventoryKey.SAND_CONTENT: float
354
+ },
355
+ ...
356
+ }
357
+ ```
358
+
359
+ The returned `kwargs` should be a dict with the shape:
360
+ ```
361
+ {
362
+ "sand_content": float
363
+ }
364
+ ```
365
+
366
+ Parameters
367
+ ----------
368
+ site : dict
369
+ A Hestia `Site` node, see: https://www.hestia.earth/schema/Site.
370
+
371
+ Returns
372
+ -------
373
+ tuple[bool, dict, dict]
374
+ A tuple containing `(should_run_, inventory, kwargs)`.
375
+ """
376
+ site_type = site.get("siteType", "")
377
+ measurement_nodes = site.get("measurements", [])
378
+ cycles = related_cycles(site)
379
+
380
+ has_measurements = len(measurement_nodes) > 0
381
+ has_related_cycles = len(cycles) > 0
382
+ has_functional_unit_1_ha = all(cycle.get("functionalUnit") in _VALID_FUNCTIONAL_UNITS for cycle in cycles)
383
+
384
+ should_compile_inventory = all([
385
+ site_type in _VALID_SITE_TYPES,
386
+ has_measurements,
387
+ has_related_cycles,
388
+ check_cycle_site_ids_identical(cycles),
389
+ has_functional_unit_1_ha
390
+ ])
391
+
392
+ inventory, kwargs = (
393
+ _compile_inventory(cycles, measurement_nodes)
394
+ if should_compile_inventory else ({}, {})
395
+ )
396
+ kwargs["seed"] = gen_seed(site)
397
+
398
+ valid_years = [year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN)]
399
+
400
+ should_run_ = all([
401
+ len(valid_years) >= _RUN_IN_PERIOD,
402
+ check_consecutive(valid_years),
403
+ any(inventory.get(year).get(_InventoryKey.SAND_CONTENT) for year in valid_years) or kwargs.get("sand_content")
404
+ ])
405
+
406
+ logs = {
407
+ "site_type": site_type,
408
+ "has_measurements": has_measurements,
409
+ "has_related_cycles": has_related_cycles,
410
+ "has_functional_unit_1_ha": has_functional_unit_1_ha,
411
+ "should_compile_inventory_tier_2": should_compile_inventory,
412
+ "should_run_tier_2": should_run_
413
+ }
414
+
415
+ return should_run_, inventory, kwargs, logs
416
+
417
+
418
+ def run(
419
+ inventory: dict[int: dict[_InventoryKey: Any]],
420
+ *,
421
+ iterations: int,
422
+ run_in_period: int = 5,
423
+ sand_content: float = 0.33,
424
+ seed: Union[int, random.Generator, None] = None,
425
+ **_
426
+ ) -> tuple[list[int], NDArray, NDArray, NDArray]:
427
+ """
428
+ Run the IPCC Tier 2 SOC model on a time series of annual data about a site and the mangagement activities taking
429
+ place on it. To avoid any errors, the `inventory` parameter must be pre-validated by the `should_run` function.
430
+
431
+ The inventory should be in the following shape:
432
+ ```
433
+ {
434
+ year (int): {
435
+ _InventoryKey.SHOULD_RUN: bool,
436
+ _InventoryKey.TEMP_MONTHLY: list[float],
437
+ _InventoryKey.PRECIP_MONTHLY: list[float],
438
+ _InventoryKey.PET_MONTHLY: list[float],
439
+ _InventoryKey.IRRIGATED_MONTHLY: list[bool]
440
+ _InventoryKey.CARBON_INPUT: float,
441
+ _InventoryKey.N_CONTENT: float,
442
+ _InventoryKey.TILLAGE_CATEGORY: IpccManagementCategory,
443
+ _InventoryKey.SAND_CONTENT: float
444
+ },
445
+ ...
446
+ }
447
+ ```
448
+
449
+ TODO: interpolate between `sandContent` measurements for different years of the inventory
450
+
451
+ Parameters
452
+ ----------
453
+ inventory : dict
454
+ The inventory built by the `should_run` function.
455
+ iterations : int
456
+ Number of iterations to run the model for.
457
+ run_in_period : int, optional
458
+ The length of the run-in period in years, must be greater than or equal to 1. Default value: `5`.
459
+ sand_content : float, optional
460
+ A back-up sand content for if none are found in the inventory, decimal proportion. Default value: `0.33`.
461
+ seed : int | Generator | None, optional
462
+ A seed to initialize the BitGenerator. If passed a Generator, it will be returned unaltered. If `None`, then
463
+ fresh, unpredictable entropy will be pulled from the OS.
464
+
465
+ Returns
466
+ -------
467
+ list[dict]
468
+ A list of HESTIA nodes containing model output results.
469
+ """
470
+ valid_inventory = {
471
+ year: group for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN)
472
+ }
473
+
474
+ def _unpack_inventory(inventory_key: _InventoryKey, monthly: bool = False) -> NDArray:
475
+ """
476
+ Unpack the inventory dict into numpy arrays with the correct shape.
477
+ """
478
+ unpacked = [group[inventory_key] for group in valid_inventory.values()]
479
+ arr = array(flatten(unpacked) if monthly else unpacked)
480
+ return repeat_1d_array_as_columns(iterations, arr)
481
+
482
+ timestamps = [year for year in valid_inventory.keys()]
483
+
484
+ temperature_monthly = _unpack_inventory(_InventoryKey.TEMP_MONTHLY, monthly=True)
485
+ precipitation_monthly = _unpack_inventory(_InventoryKey.PRECIP_MONTHLY, monthly=True)
486
+ pet_monthly = _unpack_inventory(_InventoryKey.PET_MONTHLY, monthly=True)
487
+ irrigated_monthly = _unpack_inventory(_InventoryKey.IRRIGATED_MONTHLY, monthly=True)
488
+
489
+ carbon_input_annual = _unpack_inventory(_InventoryKey.CARBON_INPUT)
490
+ n_content_annual = _unpack_inventory(_InventoryKey.N_CONTENT)
491
+ lignin_content_annual = _unpack_inventory(_InventoryKey.LIGNIN_CONTENT)
492
+
493
+ tillage_category_annual = [group[_InventoryKey.TILLAGE_CATEGORY] for group in valid_inventory.values()]
494
+
495
+ sand_content = next(
496
+ (
497
+ group[_InventoryKey.SAND_CONTENT] for group in valid_inventory.values()
498
+ if _InventoryKey.SAND_CONTENT in group
499
+ ),
500
+ sand_content
501
+ )
502
+
503
+ # --- SAMPLE PARAMETERS ---
504
+
505
+ rng = random.default_rng(seed)
506
+
507
+ active_decay_factor = _sample_parameter(iterations, _Parameter.ACTIVE_DECAY_FACTOR, seed=rng)
508
+ slow_decay_factor = _sample_parameter(iterations, _Parameter.SLOW_DECAY_FACTOR, seed=rng)
509
+ passive_decay_factor = _sample_parameter(iterations, _Parameter.PASSIVE_DECAY_FACTOR, seed=rng)
510
+ f_1 = _sample_parameter(iterations, _Parameter.F_1, seed=rng)
511
+ f_2_full_tillage = _sample_parameter(iterations, _Parameter.F_2_FULL_TILLAGE, seed=rng)
512
+ f_2_reduced_tillage = _sample_parameter(iterations, _Parameter.F_2_REDUCED_TILLAGE, seed=rng)
513
+ f_2_no_tillage = _sample_parameter(iterations, _Parameter.F_2_NO_TILLAGE, seed=rng)
514
+ f_2_unknown_tillage = _sample_parameter(iterations, _Parameter.F_2_UNKNOWN_TILLAGE, seed=rng)
515
+ f_3 = _sample_parameter(iterations, _Parameter.F_3, seed=rng)
516
+ f_5 = _sample_parameter(iterations, _Parameter.F_5, seed=rng)
517
+ f_6 = _sample_parameter(iterations, _Parameter.F_6, seed=rng)
518
+ f_7 = _sample_parameter(iterations, _Parameter.F_7, seed=rng)
519
+ f_8 = _sample_parameter(iterations, _Parameter.F_8, seed=rng)
520
+ tillage_factor_full_tillage = _sample_parameter(iterations, _Parameter.TILLAGE_FACTOR_FULL_TILLAGE, seed=rng)
521
+ tillage_factor_reduced_tillage = _sample_parameter(iterations, _Parameter.TILLAGE_FACTOR_REDUCED_TILLAGE, seed=rng)
522
+ tillage_factor_no_tillage = _sample_parameter(iterations, _Parameter.TILLAGE_FACTOR_NO_TILLAGE, seed=rng)
523
+ maximum_temperature = _sample_parameter(iterations, _Parameter.MAXIMUM_TEMPERATURE, seed=rng)
524
+ optimum_temperature = _sample_parameter(iterations, _Parameter.OPTIMUM_TEMPERATURE, seed=rng)
525
+ water_factor_slope = _sample_parameter(iterations, _Parameter.WATER_FACTOR_SLOPE, seed=rng)
526
+
527
+ f_4 = _calc_f_4(sand_content, f_5)
528
+
529
+ # --- CALCULATE TILLAGE AND CLIMATE FACTORS ---
530
+
531
+ f_2_annual = _get_f_2_annual(
532
+ tillage_category_annual, f_2_full_tillage, f_2_reduced_tillage, f_2_no_tillage, f_2_unknown_tillage
533
+ )
534
+
535
+ tillage_factor_annual = _get_tillage_factor_annual(
536
+ tillage_category_annual, tillage_factor_full_tillage, tillage_factor_reduced_tillage, tillage_factor_no_tillage
537
+ )
538
+
539
+ temperature_factor_annual = _calc_temperature_factor_annual(
540
+ temperature_monthly, maximum_temperature, optimum_temperature
541
+ )
542
+
543
+ water_factor_annual = _calc_water_factor_annual(
544
+ precipitation_monthly, pet_monthly, irrigated_monthly, water_factor_slope
545
+ )
546
+
547
+ # --- AVERAGE RUN-IN YEARS TO STABILISE INITIAL SOC STOCK ---
548
+
549
+ timestamps_ = timestamps[run_in_period - 1:] # Last year of run in becomes first year of results.
550
+
551
+ temperature_factors = avg_run_in_columnwise(temperature_factor_annual, run_in_period)
552
+ water_factors = avg_run_in_columnwise(water_factor_annual, run_in_period)
553
+ carbon_inputs = avg_run_in_columnwise(carbon_input_annual, run_in_period)
554
+ n_contents = avg_run_in_columnwise(n_content_annual, run_in_period)
555
+ lignin_contents = avg_run_in_columnwise(lignin_content_annual, run_in_period)
556
+ f_2s = avg_run_in_columnwise(f_2_annual, run_in_period)
557
+ tillage_factors = avg_run_in_columnwise(tillage_factor_annual, run_in_period)
558
+
559
+ shape = temperature_factors.shape
560
+
561
+ # --- CALCULATE THE ACTIVE ACTIVE POOL STEADY STATES ---
562
+
563
+ alphas = _calc_alpha(
564
+ carbon_inputs, f_2s, f_4, lignin_contents, n_contents, f_1, f_3, f_5, f_6, f_7, f_8
565
+ )
566
+
567
+ active_pool_decay_rates = _calc_active_pool_decay_rate(
568
+ temperature_factors, water_factors, tillage_factors, sand_content, active_decay_factor
569
+ )
570
+
571
+ active_pool_steady_states = _calc_active_pool_steady_state(
572
+ alphas, active_pool_decay_rates
573
+ )
574
+
575
+ # --- CALCULATE THE SLOW POOL STEADY STATES ---
576
+
577
+ slow_pool_decay_rates = _calc_slow_pool_decay_rate(
578
+ temperature_factors, water_factors, tillage_factors, slow_decay_factor
579
+ )
580
+
581
+ slow_pool_steady_states = _calc_slow_pool_steady_state(
582
+ carbon_inputs, f_4, active_pool_steady_states, active_pool_decay_rates, slow_pool_decay_rates,
583
+ lignin_contents, f_3
584
+ )
585
+
586
+ # --- CALCULATE THE PASSIVE POOL STEADY STATES ---
587
+
588
+ passive_pool_decay_rates = _calc_passive_pool_decay_rate(
589
+ temperature_factors, water_factors, passive_decay_factor
590
+ )
591
+
592
+ passive_pool_steady_states = _calc_passive_pool_steady_state(
593
+ active_pool_steady_states, slow_pool_steady_states, active_pool_decay_rates, slow_pool_decay_rates,
594
+ passive_pool_decay_rates, f_5, f_6
595
+ )
596
+
597
+ # --- CALCULATE THE ACTIVE, SLOW AND PASSIVE POOL SOC STOCKS ---
598
+
599
+ active_pool_soc_stocks = empty(shape)
600
+ slow_pool_soc_stocks = empty(shape)
601
+ passive_pool_soc_stocks = empty(shape)
602
+
603
+ active_pool_soc_stocks[0] = active_pool_steady_states[0]
604
+ slow_pool_soc_stocks[0] = slow_pool_steady_states[0]
605
+ passive_pool_soc_stocks[0] = passive_pool_steady_states[0]
606
+
607
+ for index in range(1, len(timestamps_)):
608
+ active_pool_soc_stocks[index] = _calc_sub_pool_soc_stock(
609
+ active_pool_steady_states[index],
610
+ active_pool_soc_stocks[index - 1],
611
+ active_pool_decay_rates[index]
612
+ )
613
+
614
+ slow_pool_soc_stocks[index] = _calc_sub_pool_soc_stock(
615
+ slow_pool_steady_states[index],
616
+ slow_pool_soc_stocks[index - 1],
617
+ slow_pool_decay_rates[index]
618
+ )
619
+
620
+ passive_pool_soc_stocks[index] = _calc_sub_pool_soc_stock(
621
+ passive_pool_steady_states[index],
622
+ passive_pool_soc_stocks[index - 1],
623
+ passive_pool_decay_rates[index]
624
+ )
625
+
626
+ # --- ADD THE POOLS AND RETURN THE RESULT ---
627
+
628
+ soc_stocks = active_pool_soc_stocks + slow_pool_soc_stocks + passive_pool_soc_stocks
629
+
630
+ descriptive_stats = calc_descriptive_stats(
631
+ soc_stocks,
632
+ STATS_DEFINITION,
633
+ axis=1, # Calculate stats rowwise.
634
+ decimals=6 # Round values to the nearest milligram.
635
+ )
636
+
637
+ return [_measurement(timestamps_, descriptive_stats)]
638
+
639
+
640
+ def _calc_temperature_factor_annual(
641
+ temperature_monthly: NDArray,
642
+ maximum_temperature: NDArray = array(45),
643
+ optimum_temperature: NDArray = array(33.69)
644
+ ) -> NDArray:
645
+ """
646
+ Equation 5.0E part 2, Temperature effect on decomposition for mineral soils using the steady-state method, Page
647
+ 5.22, Tier 2 Steady State Method for Mineral Soils, Chapter 5 Cropland, 2019 Refinement to the 2006 IPCC Guidelines
648
+ for National Greenhouse Gas Inventories.
649
+
650
+ Parameters
651
+ ----------
652
+ monthly_temperature : NDArray
653
+ Monthly average air temprature, degrees C.
654
+ maximum_temperature : NDArray
655
+ Maximum monthly air temperature for decomposition, degrees C. Default value: `[45]`.
656
+ optimum_temperature : NDArray
657
+ Optimum air temperature for decomposition, degrees C. Default value: `[33.69]`.
658
+
659
+ Returns
660
+ -------
661
+ NDArray
662
+ Annual average air temperature effect on decomposition, dimensionless.
663
+ """
664
+ mask = temperature_monthly <= maximum_temperature
665
+ prelim: NDArray = (maximum_temperature - temperature_monthly) / (maximum_temperature - optimum_temperature)
666
+
667
+ temperature_factor_monthly = empty(temperature_monthly.shape)
668
+ temperature_factor_monthly[mask] = pow(prelim[mask], 0.2) * exp((0.2 / 2.63) * (1 - pow(prelim[mask], 2.63)))
669
+ temperature_factor_monthly[~mask] = 0
670
+
671
+ return grouped_avg(temperature_factor_monthly, n=12)
672
+
673
+
674
+ def _calc_water_factor_annual(
675
+ precipitation_monthly: NDArray,
676
+ pet_monthly: NDArray,
677
+ irrigated_monthly: NDArray = array(False),
678
+ water_factor_slope: NDArray = array(1.331),
679
+ ) -> NDArray:
680
+ """
681
+ Equation 5.0F, part 1. Calculate the average annual water effect on decomposition in mineral soils using the
682
+ Steady-State Method multiplied by a coefficient of `1.5`.
683
+
684
+ Parameters
685
+ ----------
686
+ precipitation_monthly : NDArray
687
+ Monthly sum total precipitation, mm.
688
+ pet_monthly : NDArray
689
+ Monthly sum total potential evapotranspiration, mm.
690
+ is_irrigated_monthly : NDArray
691
+ Monthly true/false value that describe whether or not irrigation was used.
692
+ water_factor_slope : NDArray
693
+ The slope for mappet term to estimate water factor, dimensionless. Default value: `[1.331]`.
694
+
695
+ Returns
696
+ -------
697
+ NDArray
698
+ Annual water effect on decomposition, dimensionless.
699
+ """
700
+ MAX_MAPPET = 1.25
701
+ WATER_FACTOR_IRRIGATED = 0.775
702
+
703
+ shape = pet_monthly.shape
704
+ mask = pet_monthly != 0
705
+
706
+ mappet_monthly = empty(shape)
707
+ mappet_monthly[mask] = minimum(precipitation_monthly[mask] / pet_monthly[mask], MAX_MAPPET)
708
+ mappet_monthly[~mask] = MAX_MAPPET
709
+
710
+ water_factor_monthly = where(
711
+ irrigated_monthly,
712
+ WATER_FACTOR_IRRIGATED,
713
+ 0.2129 + (water_factor_slope * mappet_monthly) - (0.2413 * mappet_monthly**2)
714
+ )
715
+
716
+ return 1.5 * grouped_avg(water_factor_monthly, n=12)
717
+
718
+
719
+ def _calc_f_4(sand_content: NDArray = array(0.33), f_5: NDArray = array(0.0855)) -> NDArray:
720
+ """
721
+ Equation 5.0C, part 4. Calculate the value of the stabilisation efficiencies for active pool decay products
722
+ entering the slow pool based on the sand content of the soil.
723
+
724
+ Parameters
725
+ ----------
726
+ sand_content : NDArray
727
+ The sand content of the soil, decimal proportion. Default value: `[0.33]`.
728
+ f_5 : NDArray
729
+ The stabilisation efficiencies for active pool decay products entering the passive pool, decimal_proportion.
730
+ Default value: `[0.0855]`.
731
+
732
+ Returns
733
+ -------
734
+ NDArray
735
+ The stabilisation efficiencies for active pool decay products entering the slow pool, decimal proportion.
736
+ """
737
+ return 1 - f_5 - (0.17 + 0.68 * sand_content)
738
+
739
+
740
+ def _get_f_2_annual(
741
+ tillage_category_annual: list[IpccManagementCategory],
742
+ f_2_full_tillage: NDArray = array(0.455),
743
+ f_2_reduced_tillage: NDArray = array(0.477),
744
+ f_2_no_tillage: NDArray = array(0.5),
745
+ f_2_unknown_tillage: NDArray = array(0.368),
746
+ ) -> NDArray:
747
+ """
748
+ Get the value of `f_2` (the stabilisation efficiencies for structural decay products entering the active pool)
749
+ based on the tillage `IpccManagementCategory`.
750
+
751
+ If tillage regime is unknown, `IpccManagementCategory.OTHER` should be assumed.
752
+
753
+ Parameters
754
+ ----------
755
+ tillage_category_annual : list[IpccManagementCategory]
756
+ The tillage category for each year in the inventory.
757
+ f_2_full_tillage : NDArray
758
+ The stabilisation efficiencies for structural decay products entering the active pool under full tillage,
759
+ decimal proportion. Default value: `[0.455]`.
760
+ f_2_reduced_tillage : NDArray
761
+ The stabilisation efficiencies for structural decay products entering the active pool under reduced tillage,
762
+ decimal proportion. Default value: `[0.477]`.
763
+ f_2_no_tillage : NDArray
764
+ The stabilisation efficiencies for structural decay products entering the active pool under no tillage,
765
+ decimal proportion. Default value: `[0.5]`.
766
+ f_2_unknown_tillage : NDArray
767
+ The stabilisation efficiencies for structural decay products entering the active pool if tillage is not known,
768
+ decimal proportion. Default value: `[0.368]`.
769
+
770
+ Returns
771
+ -------
772
+ NDArray
773
+ The stabilisation efficiencies for structural decay products entering the active pool, decimal proportion.
774
+ """
775
+ ipcc_tillage_management_category_to_f_2s = {
776
+ IpccManagementCategory.FULL_TILLAGE: f_2_full_tillage,
777
+ IpccManagementCategory.REDUCED_TILLAGE: f_2_reduced_tillage,
778
+ IpccManagementCategory.NO_TILLAGE: f_2_no_tillage,
779
+ IpccManagementCategory.OTHER: f_2_unknown_tillage
780
+ }
781
+ default = f_2_unknown_tillage
782
+ return vstack([ipcc_tillage_management_category_to_f_2s.get(till, default) for till in tillage_category_annual])
783
+
784
+
785
+ def _get_tillage_factor_annual(
786
+ tillage_category_annual: list[IpccManagementCategory],
787
+ tillage_factor_full_tillage: NDArray = array(3.036),
788
+ tillage_factor_reduced_tillage: NDArray = array(2.075),
789
+ tillage_factor_no_tillage: NDArray = array(1)
790
+ ) -> NDArray:
791
+ """
792
+ Calculate the tillage disturbance modifier on decay rate for active and slow sub-pools based on the tillage
793
+ `IpccManagementCategory`.
794
+
795
+ If tillage regime is unknown, `FULL_TILLAGE` should be assumed.
796
+
797
+ Parameters
798
+ ----------
799
+ tillage_category_annual : list[IpccManagementCategory]
800
+ The tillage category for each year in the inventory.
801
+ tillage_factor_full_tillage : NDArray
802
+ The tillage disturbance modifier for decay rates under full tillage, dimensionless. Default value: `[3.036]`.
803
+ tillage_factor_reduced_tillage : NDArray
804
+ Tillage disturbance modifier for decay rates under reduced tillage, dimensionless. Default value: `[2.075]`.
805
+ tillage_factor_no_tillage : NDArray
806
+ Tillage disturbance modifier for decay rates under no tillage, dimensionless. Default value: `[1]`.
807
+
808
+ Returns
809
+ -------
810
+ NDArray
811
+ The tillage disturbance modifier on decay rate for active and slow sub-pools, dimensionless.
812
+ """
813
+ ipcc_tillage_management_category_to_tillage_factors = {
814
+ IpccManagementCategory.FULL_TILLAGE: tillage_factor_full_tillage,
815
+ IpccManagementCategory.REDUCED_TILLAGE: tillage_factor_reduced_tillage,
816
+ IpccManagementCategory.NO_TILLAGE: tillage_factor_no_tillage,
817
+ }
818
+ default = tillage_factor_full_tillage
819
+ return vstack([
820
+ ipcc_tillage_management_category_to_tillage_factors.get(till, default)
821
+ for till in tillage_category_annual
822
+ ])
823
+
824
+
825
+ def _calc_alpha(
826
+ carbon_input: NDArray,
827
+ f_2: NDArray,
828
+ f_4: NDArray,
829
+ lignin_content: NDArray = array(0.073),
830
+ nitrogen_content: NDArray = array(0.0083),
831
+ f_1: NDArray = array(0.378),
832
+ f_3: NDArray = array(0.455),
833
+ f_5: NDArray = array(0.0855),
834
+ f_6: NDArray = array(0.0504),
835
+ f_7: NDArray = array(0.42),
836
+ f_8: NDArray = array(0.45)
837
+ ) -> NDArray:
838
+ """
839
+ Equation 5.0G, part 1. Calculate the C input to the active soil carbon sub-pool, kg C ha-1.
840
+
841
+ See table 5.5b for default values for lignin content and nitrogen content.
842
+
843
+ Parameters
844
+ ----------
845
+ carbon_input : NDArray
846
+ Total carbon input to the soil, kg C ha-1.
847
+ f_2 : NDArray
848
+ The stabilisation efficiencies for structural decay products entering the active pool, decimal proportion.
849
+ f_4 : NDArray
850
+ The stabilisation efficiencies for active pool decay products entering the slow pool, decimal proportion.
851
+ lignin_content : NDArray
852
+ The average lignin content of carbon input sources, decimal proportion. Default value: `[0.073]`.
853
+ nitrogen_content : NDArray
854
+ The average nitrogen content of carbon input sources, decimal proportion. Default value: `[0.0083]`.
855
+ f_1 : NDArray
856
+ The stabilisation efficiencies for metabolic decay products entering the active pool, decimal proportion.
857
+ Default value: `[0.378]`.
858
+ f_3 : NDArray
859
+ The stabilisation efficiencies for structural decay products entering the slow pool, decimal proportion.
860
+ Default value: `[0.455]`.
861
+ f_5 : NDArray
862
+ The stabilisation efficiencies for active pool decay products entering the passive pool, decimal proportion.
863
+ Default value: `[0.0855]`.
864
+ f_6 : NDArray
865
+ The stabilisation efficiencies for slow pool decay products entering the passive pool, decimal proportion.
866
+ Default value: `[0.0504]`.
867
+ f_7 : NDArray
868
+ The stabilisation efficiencies for slow pool decay products entering the active pool, decimal proportion.
869
+ Default value: `[0.42]`.
870
+ f_8 : NDArray
871
+ The stabilisation efficiencies for passive pool decay products entering the active pool, decimal proportion.
872
+ Default value: `[0.45]`.
873
+
874
+ Returns
875
+ -------
876
+ NDArray
877
+ The C input to the active soil carbon sub-pool, kg C ha-1.
878
+ """
879
+ beta = _calc_beta(
880
+ carbon_input, lignin_content=lignin_content, nitrogen_content=nitrogen_content
881
+ )
882
+
883
+ x = beta * f_1
884
+ y = (carbon_input * (1 - lignin_content) - beta) * f_2
885
+ z = (carbon_input * lignin_content) * f_3 * (f_7 + (f_6 * f_8))
886
+ d = 1 - (f_4 * f_7) - (f_5 * f_8) - (f_4 * f_6 * f_8)
887
+ return (x + y + z) / d
888
+
889
+
890
+ def _calc_beta(
891
+ carbon_input: NDArray,
892
+ lignin_content: NDArray = array(0.073),
893
+ nitrogen_content: NDArray = array(0.0083),
894
+ ) -> NDArray:
895
+ """
896
+ Equation 5.0G, part 2. Calculate the C input to the metabolic dead organic matter C component, kg C ha-1.
897
+
898
+ See table 5.5b for default values for lignin content and nitrogen content.
899
+
900
+ Parameters
901
+ ----------
902
+ carbon_input : NDArray
903
+ Total carbon input to the soil, kg C ha-1.
904
+ lignin_content : NDArray
905
+ The average lignin content of carbon input sources, decimal proportion. Default value: `[0.073]`.
906
+ nitrogen_content : NDArray
907
+ The average nitrogen content of carbon sources, decimal proportion. Default value: `[0.0083]`.
908
+
909
+ Returns
910
+ -------
911
+ NDArray
912
+ The C input to the metabolic dead organic matter C component, kg C ha-1.
913
+ """
914
+ return carbon_input * (0.85 - 0.018 * (lignin_content / nitrogen_content))
915
+
916
+
917
+ def _calc_active_pool_decay_rate(
918
+ temperature_factor_annual: NDArray,
919
+ water_factor_annual: NDArray,
920
+ tillage_factor: NDArray,
921
+ sand_content: NDArray = array(0.33),
922
+ active_decay_factor: NDArray = array(7.4),
923
+ ) -> NDArray:
924
+ """
925
+ Equation 5.0B, part 3. Calculate the decay rate for the active SOC sub-pool given conditions in an inventory year.
926
+
927
+ Parameters
928
+ ----------
929
+ temperature_factor_annual : NDArray
930
+ Average annual temperature factor, dimensionless. All elements between `0` and `1`.
931
+ water_factor_annual : NDArray
932
+ Average annual water factor, dimensionless. All elements between `0.31935` and `2.25`.
933
+ tillage_factor : NDArray
934
+ The tillage disturbance modifier on decay rate for active and slow sub-pools, dimensionless.
935
+ sand_content : NDArray
936
+ The sand content of the soil, decimal proportion. Default value: `[0.33]`.
937
+ active_decay_factor : NDArray
938
+ decay rate constant under optimal conditions for decomposition of the active SOC subpool, year-1. Default value:
939
+ `[7.4]`.
940
+
941
+ Returns
942
+ -------
943
+ NDArray
944
+ The decay rate for active SOC sub-pool, year-1.
945
+ """
946
+ sand_factor = 0.25 + (0.75 * sand_content)
947
+ return (
948
+ temperature_factor_annual
949
+ * water_factor_annual
950
+ * tillage_factor
951
+ * sand_factor
952
+ * active_decay_factor
953
+ )
954
+
955
+
956
+ def _calc_active_pool_steady_state(
957
+ alpha: NDArray, active_pool_decay_rate: NDArray
958
+ ) -> NDArray:
959
+ """
960
+ Equation 5.0B part 2. Calculate the steady state active sub-pool SOC stock given conditions in an inventory year.
961
+
962
+ Parameters
963
+ ----------
964
+ alpha : NDArray
965
+ The C input to the active soil carbon sub-pool, kg C ha-1.
966
+ active_pool_decay_rate : NDArray
967
+ Decay rate for active SOC sub-pool, year-1.
968
+
969
+ Returns
970
+ -------
971
+ NDArray
972
+ The steady state active sub-pool SOC stock given conditions in year y, kg C ha-1
973
+ """
974
+ return alpha / active_pool_decay_rate
975
+
976
+
977
+ def _calc_slow_pool_decay_rate(
978
+ temperature_factor_annual: NDArray,
979
+ water_factor_annual: NDArray,
980
+ tillage_factor: NDArray,
981
+ slow_decay_factor: NDArray = array(0.209),
982
+ ) -> NDArray:
983
+ """
984
+ Equation 5.0C, part 3. Calculate the decay rate for the slow SOC sub-pool given conditions in an inventory year.
985
+
986
+ Parameters
987
+ ----------
988
+ temperature_factor_annual : NDArray
989
+ Average annual temperature factor, dimensionless. All elements between `0` and `1`.
990
+ water_factor_annual : NDArray
991
+ Average annual water factor, dimensionless. All elements between `0.31935` and `2.25`.
992
+ tillage_factor : NDArray
993
+ The tillage disturbance modifier on decay rate for active and slow sub-pools, dimensionless.
994
+ slow_decay_factor : NDArray
995
+ The decay rate constant under optimal conditions for decomposition of the slow SOC subpool, year-1.
996
+ Default value: `0.209`.
997
+
998
+ Returns
999
+ -------
1000
+ NDArray
1001
+ The decay rate for slow SOC sub-pool, year-1.
1002
+ """
1003
+ return (
1004
+ temperature_factor_annual
1005
+ * water_factor_annual
1006
+ * tillage_factor
1007
+ * slow_decay_factor
1008
+ )
1009
+
1010
+
1011
+ def _calc_slow_pool_steady_state(
1012
+ carbon_input: NDArray,
1013
+ f_4: NDArray,
1014
+ active_pool_steady_state: NDArray,
1015
+ active_pool_decay_rate: NDArray,
1016
+ slow_pool_decay_rate: NDArray,
1017
+ lignin_content: NDArray = array(0.073),
1018
+ f_3: NDArray = array(0.455),
1019
+ ) -> NDArray:
1020
+ """
1021
+ Equation 5.0C, part 2. Calculate the steady state slow sub-pool SOC stock given conditions in an inventory year.
1022
+
1023
+ Parameters
1024
+ ----------
1025
+ carbon_input : NDArray
1026
+ Total carbon input to the soil, kg C ha-1.
1027
+ f_4 : NDArray
1028
+ The stabilisation efficiencies for active pool decay products entering the slow pool, decimal proportion.
1029
+ active_pool_steady_state : NDArray
1030
+ The steady state active sub-pool SOC stock given conditions in year y, kg C ha-1
1031
+ active_pool_decay_rate : NDArray
1032
+ Decay rate for active SOC sub-pool, year-1.
1033
+ slow_pool_decay_rate : NDArray
1034
+ Decay rate for slow SOC sub-pool, year-1.
1035
+ lignin_content : NDArray
1036
+ The average lignin content of carbon input sources, decimal proportion. Default value: `[0.073]`.
1037
+ f_3 : NDArray
1038
+ The stabilisation efficiencies for structural decay products entering the slow pool, decimal proportion.
1039
+ Default value: `[0.455]`.
1040
+
1041
+ Returns
1042
+ -------
1043
+ NDArray
1044
+ The steady state slow sub-pool SOC stock given conditions in year y, kg C ha-1.
1045
+ """
1046
+ x = carbon_input * lignin_content * f_3
1047
+ y = active_pool_steady_state * active_pool_decay_rate * f_4
1048
+ return (x + y) / slow_pool_decay_rate
1049
+
1050
+
1051
+ def _calc_passive_pool_decay_rate(
1052
+ temperature_factor_annual: NDArray,
1053
+ water_factor_annual: NDArray,
1054
+ passive_decay_factor: NDArray = array(0.00689),
1055
+ ) -> NDArray:
1056
+ """
1057
+ Equation 5.0D, part 3. Calculate the decay rate for the passive SOC sub-pool given conditions in an inventory year.
1058
+
1059
+ Parameters
1060
+ ----------
1061
+ temperature_factor_annual : NDArray
1062
+ Average annual temperature factor, dimensionless. All elements between `0` and `1`.
1063
+ water_factor_annual : NDArray
1064
+ Average annual water factor, dimensionless. All elements between `0.31935` and `2.25`.
1065
+ passive_decay_factor : NDArray
1066
+ decay rate constant under optimal conditions for decomposition of the passive SOC subpool, year-1.
1067
+ Default value: `[0.00689]`.
1068
+
1069
+ Returns
1070
+ -------
1071
+ NDArray
1072
+ The decay rate for passive SOC sub-pool, year-1.
1073
+ """
1074
+ return temperature_factor_annual * water_factor_annual * passive_decay_factor
1075
+
1076
+
1077
+ def _calc_passive_pool_steady_state(
1078
+ active_pool_steady_state: NDArray,
1079
+ slow_pool_steady_state: NDArray,
1080
+ active_pool_decay_rate: NDArray,
1081
+ slow_pool_decay_rate: NDArray,
1082
+ passive_pool_decay_rate: NDArray,
1083
+ f_5: NDArray = array(0.0855),
1084
+ f_6: NDArray = array(0.0504),
1085
+ ) -> NDArray:
1086
+ """
1087
+ Equation 5.0D, part 2. Calculate the steady state passive sub-pool SOC stock given conditions in an inventory year.
1088
+
1089
+ Parameters
1090
+ ----------
1091
+ active_pool_steady_state : NDArray
1092
+ The steady state active sub-pool SOC stock given conditions in year y, kg C ha-1.
1093
+ slow_pool_steady_state : NDArray
1094
+ The steady state slow sub-pool SOC stock given conditions in year y, kg C ha-1.
1095
+ active_pool_decay_rate : NDArray
1096
+ Decay rate for active SOC sub-pool, year-1.
1097
+ slow_pool_decay_rate : NDArray
1098
+ Decay rate for slow SOC sub-pool, year-1.
1099
+ passive_pool_decay_rate : NDArray
1100
+ Decay rate for passive SOC sub-pool, year-1.
1101
+ f_5 : NDArray
1102
+ The stabilisation efficiencies for active pool decay products entering the passive pool, decimal proportion.
1103
+ Default value: `[0.0855]`.
1104
+ f_6 : NDArray
1105
+ The stabilisation efficiencies for slow pool decay products entering the passive pool, decimal proportion.
1106
+ Default value: `[0.0504]`.
1107
+
1108
+ Returns
1109
+ -------
1110
+ NDArray
1111
+ The steady state passive sub-pool SOC stock given conditions in year y, kg C ha-1.
1112
+ """
1113
+ x = active_pool_steady_state * active_pool_decay_rate * f_5
1114
+ y = slow_pool_steady_state * slow_pool_decay_rate * f_6
1115
+ return (x + y) / passive_pool_decay_rate
1116
+
1117
+
1118
+ def _calc_sub_pool_soc_stock(
1119
+ sub_pool_steady_state: NDArray,
1120
+ previous_sub_pool_soc_stock: NDArray,
1121
+ sub_pool_decay_rate: NDArray,
1122
+ timestep: int = 1,
1123
+ ) -> NDArray:
1124
+ """
1125
+ 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.
1126
+
1127
+ If `sub_pool_decay_rate > 1` then set its value to `1` for this calculation.
1128
+
1129
+ Parameters
1130
+ ----------
1131
+ sub_pool_steady_state : NDArray
1132
+ The steady state sub-pool SOC stock given conditions in year y, kg C ha-1.
1133
+ previous_sub_pool_soc_stock : NDArray
1134
+ The sub-pool SOC stock in year y-timestep (by default one year ago), kg C ha-1.
1135
+ sub_pool_decay_rate : NDArray
1136
+ Decay rate for active SOC sub-pool, year-1.
1137
+ timestep : int
1138
+ The number of years between current and previous inventory year. Default value = `1`.
1139
+
1140
+ Returns
1141
+ -------
1142
+ NDArray
1143
+ The sub-pool SOC stock in year y, kg C ha-1.
1144
+ """
1145
+ sub_pool_decay_rate = minimum(1, sub_pool_decay_rate)
1146
+ return (
1147
+ previous_sub_pool_soc_stock
1148
+ + (sub_pool_steady_state - previous_sub_pool_soc_stock)
1149
+ * timestep
1150
+ * sub_pool_decay_rate
1151
+ )
1152
+
1153
+
1154
+ # --- COMPILE TIER 2 INVENTORY ---
1155
+
1156
+
1157
+ def _compile_inventory(
1158
+ cycles: list[dict], measurement_nodes: list[dict]
1159
+ ) -> tuple[dict, dict]:
1160
+ """
1161
+ Builds an annual inventory of data and a dictionary of keyword arguments for the tier 2 model.
1162
+
1163
+ TODO: implement long-term average climate data and annual climate data as back ups for monthly data
1164
+ TODO: implement randomisation for `irrigationMonthly` if `startDate` and `endDate` are not provided
1165
+ """
1166
+ grouped_cycles = group_nodes_by_year(cycles)
1167
+ grouped_measurements = group_nodes_by_year(measurement_nodes, mode=GroupNodesByYearMode.DATES)
1168
+
1169
+ grouped_climate_data = _get_grouped_climate_measurements(grouped_measurements)
1170
+ grouped_irrigated_monthly = _get_grouped_irrigated_monthly(grouped_cycles)
1171
+ grouped_sand_content_measurements = _get_grouped_sand_content_measurements(grouped_measurements)
1172
+ grouped_carbon_input_data = _get_grouped_carbon_input_data(grouped_cycles)
1173
+ grouped_tillage_categories = _get_grouped_tillage_categories(grouped_cycles)
1174
+ grouped_is_paddy_rice = _get_grouped_is_paddy_rice(grouped_cycles)
1175
+
1176
+ grouped_data = merge(
1177
+ grouped_climate_data,
1178
+ grouped_irrigated_monthly,
1179
+ grouped_sand_content_measurements,
1180
+ grouped_carbon_input_data,
1181
+ grouped_tillage_categories,
1182
+ grouped_is_paddy_rice
1183
+ )
1184
+
1185
+ grouped_should_run = {
1186
+ year: {_InventoryKey.SHOULD_RUN: _should_run_inventory(group)}
1187
+ for year, group in grouped_data.items()
1188
+ }
1189
+
1190
+ inventory = merge(grouped_data, grouped_should_run)
1191
+
1192
+ # Get a back-up value for sand content if no dated ones are available.
1193
+ sand_content = get_node_value(find_term_match(
1194
+ [m for m in measurement_nodes if m.get("depthUpper") == DEPTH_UPPER and m.get("depthLower") == DEPTH_LOWER],
1195
+ _SAND_CONTENT_TERM_ID,
1196
+ {}
1197
+ )) / 100
1198
+
1199
+ kwargs = {
1200
+ "sand_content": sand_content
1201
+ }
1202
+
1203
+ return inventory, kwargs
1204
+
1205
+
1206
+ def _check_12_months(inner_dict: dict, keys: set[Any]):
1207
+ """
1208
+ Checks whether an inner dict has 12 months of data for each of the required inner keys.
1209
+
1210
+ Parameters
1211
+ ----------
1212
+ inner_dict : dict
1213
+ A dictionary representing one year in a timeseries for the Tier 2 model.
1214
+ keys : set[Any]
1215
+ The required inner keys.
1216
+
1217
+ Returns
1218
+ -------
1219
+ bool
1220
+ Whether or not the inner dict satisfies the conditions.
1221
+ """
1222
+ return all(
1223
+ len(inner_dict.get(key, [])) == 12 for key in keys
1224
+ )
1225
+
1226
+
1227
+ def _get_grouped_climate_measurements(grouped_measurements: dict) -> dict:
1228
+ return {
1229
+ year: {
1230
+ _InventoryKey.TEMP_MONTHLY: non_empty_list(flatten(
1231
+ node.get("value", []) for node in measurements if node_term_match(node, _TEMPERATURE_MONTHLY_TERM_ID)
1232
+ )),
1233
+ _InventoryKey.PRECIP_MONTHLY: non_empty_list(flatten(
1234
+ node.get("value", []) for node in measurements if node_term_match(node, _PRECIPITATION_MONTHLY_TERM_ID)
1235
+ )),
1236
+ _InventoryKey.PET_MONTHLY: non_empty_list(flatten(
1237
+ node.get("value", []) for node in measurements if node_term_match(node, _PET_MONTHLY_TERM_ID)
1238
+ ))
1239
+ } for year, measurements in grouped_measurements.items()
1240
+ }
1241
+
1242
+
1243
+ def _get_grouped_irrigated_monthly(grouped_cycles: dict) -> dict:
1244
+ return {
1245
+ year: {
1246
+ _InventoryKey.IRRIGATED_MONTHLY: _get_irrigated_monthly(year, cycles)
1247
+ } for year, cycles in grouped_cycles.items()
1248
+ }
1249
+
1250
+
1251
+ def _get_irrigated_monthly(year: int, cycles: list[dict]) -> list[bool]:
1252
+ # Get practice nodes and add "startDate" and "endDate" from cycle if missing.
1253
+ irrigation_nodes = non_empty_list(flatten([
1254
+ [
1255
+ {
1256
+ "startDate": cycle.get("startDate"),
1257
+ "endDate": cycle.get("endDate"),
1258
+ **node
1259
+ } for node in cycle.get("practices", [])
1260
+ ] for cycle in cycles
1261
+ ]))
1262
+
1263
+ grouped_nodes = group_nodes_by_year_and_month(irrigation_nodes)
1264
+
1265
+ # For each month (1 - 12) check if irrigation is present.
1266
+ return [check_irrigation(grouped_nodes.get(year, {}).get(month, [])) for month in range(1, 13)]
1267
+
1268
+
1269
+ def _get_grouped_sand_content_measurements(grouped_measurements: dict) -> dict:
1270
+ grouped_sand_content_measurements = {
1271
+ year: find_term_match(
1272
+ [m for m in measurements if m.get("depthUpper") == DEPTH_UPPER and m.get("depthLower") == DEPTH_LOWER],
1273
+ _SAND_CONTENT_TERM_ID,
1274
+ {}
1275
+ ) for year, measurements in grouped_measurements.items()
1276
+ }
1277
+
1278
+ return {
1279
+ year: {_InventoryKey.SAND_CONTENT: get_node_value(measurement)/100}
1280
+ for year, measurement in grouped_sand_content_measurements.items() if measurement
1281
+ }
1282
+
1283
+
1284
+ def _get_grouped_carbon_input_data(grouped_cycles: dict) -> dict:
1285
+ grouped_carbon_sources = {
1286
+ year: _get_carbon_sources_from_cycles(cycle)
1287
+ for year, cycle in grouped_cycles.items()
1288
+ }
1289
+
1290
+ return {
1291
+ year: {
1292
+ _InventoryKey.CARBON_INPUT: _calc_total_organic_carbon_input(carbon_sources),
1293
+ _InventoryKey.N_CONTENT: _calc_average_nitrogen_content_of_organic_carbon_sources(carbon_sources),
1294
+ _InventoryKey.LIGNIN_CONTENT: _calc_average_lignin_content_of_organic_carbon_sources(carbon_sources)
1295
+ } for year, carbon_sources in grouped_carbon_sources.items()
1296
+ }
1297
+
1298
+
1299
+ def _get_carbon_sources_from_cycles(cycles: dict) -> list[CarbonSource]:
1300
+ """
1301
+ Retrieves and formats all of the valid carbon sources from a list of cycles.
1302
+
1303
+ Carbon sources can be either a Hestia `Product` node (e.g. crop residue) or `Input` node (e.g. organic amendment).
1304
+
1305
+ Parameters
1306
+ ----------
1307
+ cycles : list[dict]
1308
+ A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
1309
+
1310
+ Returns
1311
+ -------
1312
+ list[CarbonSource]
1313
+ A formatted list of `CarbonSource`s for the inputted `Cycle`s.
1314
+ """
1315
+ inputs_and_products = non_empty_list(flatten(
1316
+ [cycle.get("inputs", []) + cycle.get("products", []) for cycle in cycles]
1317
+ ))
1318
+
1319
+ return non_empty_list([
1320
+ _iterate_carbon_source(node) for node in inputs_and_products
1321
+ if any([
1322
+ node.get("term", {}).get("@id") in get_crop_residue_inc_or_left_terms_with_cache(),
1323
+ node.get("term", {}).get("termType") in _CARBON_SOURCE_TERM_TYPES
1324
+ ])
1325
+ ])
1326
+
1327
+
1328
+ def _iterate_carbon_source(node: dict) -> Union[CarbonSource, None]:
1329
+ """
1330
+ Validates whether a node is a valid carbon source and returns a `CarbonSource` named tuple if yes.
1331
+
1332
+ Parameters
1333
+ ----------
1334
+ node : dict
1335
+ A Hestia `Product` or `Input` node, see: https://www.hestia.earth/schema/Product
1336
+ or https://www.hestia.earth/schema/Input.
1337
+
1338
+ Returns
1339
+ -------
1340
+ CarbonSource | None
1341
+ A `CarbonSource` named tuple if the node is a carbon source with the required properties, else `None`.
1342
+ """
1343
+ mass = list_sum(node.get("value", []))
1344
+ carbon_content, nitrogen_content, lignin_content = (
1345
+ get_node_property(node, term_id).get("value", 0)/100 for term_id in _CARBON_INPUT_PROPERTY_TERM_IDS
1346
+ )
1347
+
1348
+ should_run_ = all([
1349
+ mass > 0,
1350
+ 0 < carbon_content <= 1,
1351
+ 0 < nitrogen_content <= 1,
1352
+ 0 < lignin_content <= 1
1353
+ ])
1354
+
1355
+ return (
1356
+ CarbonSource(
1357
+ mass, carbon_content, nitrogen_content, lignin_content
1358
+ ) if should_run_ else None
1359
+ )
1360
+
1361
+
1362
+ def _calc_total_organic_carbon_input(
1363
+ carbon_sources: list[CarbonSource], default_carbon_content=0.42
1364
+ ) -> float:
1365
+ """
1366
+ Equation 5.0H part 1. Calculate the total organic carbon to a site from all carbon sources (above-ground and
1367
+ below-ground crop residues, organic amendments, etc.).
1368
+
1369
+ Parameters
1370
+ ----------
1371
+ carbon_sources : list[CarbonSource])
1372
+ A list of carbon sources as named tuples with the format
1373
+ `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`.
1374
+ default_carbon_content : float
1375
+ The default carbon content of a carbon source, decimal proportion, kg C (kg d.m.)-1.
1376
+
1377
+ Returns
1378
+ -------
1379
+ float
1380
+ The total mass of organic carbon inputted into the site, kg C ha-1.
1381
+ """
1382
+ return sum(c.mass * (c.carbon_content if c.carbon_content else default_carbon_content) for c in carbon_sources)
1383
+
1384
+
1385
+ def _calc_average_nitrogen_content_of_organic_carbon_sources(
1386
+ carbon_sources: list[CarbonSource], default_nitrogen_content=0.0085
1387
+ ) -> float:
1388
+ """
1389
+ Calculate the average nitrogen content of the carbon inputs through a weighted mean.
1390
+
1391
+ Parameters
1392
+ ----------
1393
+ carbon_sources : list[CarbonSource]
1394
+ A list of carbon sources as named tuples with the format
1395
+ `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`.
1396
+ default_nitrogen_content : float
1397
+ The default nitrogen content of a carbon source, decimal proportion, kg N (kg d.m.)-1.
1398
+
1399
+ Returns
1400
+ -------
1401
+ float
1402
+ The average nitrogen content of the carbon sources, decimal_proportion, kg N (kg d.m.)-1.
1403
+ """
1404
+ total_weight = sum(c.mass for c in carbon_sources)
1405
+ weighted_values = [
1406
+ c.mass * (c.nitrogen_content if c.nitrogen_content else default_nitrogen_content) for c in carbon_sources
1407
+ ]
1408
+ should_run_ = total_weight > 0
1409
+ return sum(weighted_values) / total_weight if should_run_ else 0
1410
+
1411
+
1412
+ def _calc_average_lignin_content_of_organic_carbon_sources(
1413
+ carbon_sources: list[CarbonSource], default_lignin_content=0.073
1414
+ ) -> float:
1415
+ """
1416
+ Calculate the average lignin content of the carbon inputs through a weighted mean.
1417
+
1418
+ Parameters
1419
+ ----------
1420
+ carbon_sources : list[CarbonSource]
1421
+ A list of carbon sources as named tuples with the format
1422
+ `(mass: float, carbon_content: float, nitrogen_content: float, lignin_content: float)`.
1423
+ default_lignin_content : float
1424
+ The default lignin content of a carbon source, decimal proportion, kg lignin (kg d.m.)-1.
1425
+
1426
+ Returns
1427
+ -------
1428
+ float
1429
+ The average lignin content of the carbon sources, decimal_proportion, kg lignin (kg d.m.)-1.
1430
+ """
1431
+ total_weight = sum(c.mass for c in carbon_sources)
1432
+ weighted_values = [
1433
+ c.mass * (c.lignin_content if c.lignin_content else default_lignin_content) for c in carbon_sources
1434
+ ]
1435
+ should_run_ = total_weight > 0
1436
+ return sum(weighted_values) / total_weight if should_run_ else 0
1437
+
1438
+
1439
+ def _get_grouped_tillage_categories(grouped_cycles):
1440
+ return {
1441
+ year: {
1442
+ _InventoryKey.TILLAGE_CATEGORY: _assign_tier_2_ipcc_tillage_management_category(cycles)
1443
+ } for year, cycles in grouped_cycles.items()
1444
+ }
1445
+
1446
+
1447
+ def _assign_tier_2_ipcc_tillage_management_category(
1448
+ cycles: list[dict],
1449
+ default: IpccManagementCategory = IpccManagementCategory.OTHER
1450
+ ) -> IpccManagementCategory:
1451
+ """
1452
+ Assigns a tillage `IpccManagementCategory` to a list of Hestia `Cycle`s.
1453
+
1454
+ Parameters
1455
+ ----------
1456
+ cycles : list[dict])
1457
+ A list of Hestia `Cycle` nodes, see: https://www.hestia.earth/schema/Cycle.
1458
+
1459
+ Returns
1460
+ -------
1461
+ IpccManagementCategory: The assigned tillage `IpccManagementCategory`.
1462
+ """
1463
+ return next(
1464
+ (
1465
+ key for key in _TIER_2_TILLAGE_MANAGEMENT_CATEGORY_DECISION_TREE
1466
+ if _TIER_2_TILLAGE_MANAGEMENT_CATEGORY_DECISION_TREE[key](cycles, key)
1467
+ ),
1468
+ default
1469
+ ) if len(cycles) > 0 else default
1470
+
1471
+
1472
+ _TIER_2_TILLAGE_MANAGEMENT_CATEGORY_DECISION_TREE = {
1473
+ IpccManagementCategory.FULL_TILLAGE: (
1474
+ lambda cycles, key: any(
1475
+ _check_cycle_tillage_management_category(cycle, key) for cycle in cycles
1476
+ )
1477
+ ),
1478
+ IpccManagementCategory.REDUCED_TILLAGE: (
1479
+ lambda cycles, key: any(
1480
+ _check_cycle_tillage_management_category(cycle, key) for cycle in cycles
1481
+ )
1482
+ ),
1483
+ IpccManagementCategory.NO_TILLAGE: (
1484
+ lambda cycles, key: any(
1485
+ _check_cycle_tillage_management_category(cycle, key) for cycle in cycles
1486
+ )
1487
+ )
1488
+ }
1489
+
1490
+
1491
+ def _check_cycle_tillage_management_category(
1492
+ cycle: dict,
1493
+ key: IpccManagementCategory
1494
+ ) -> bool:
1495
+ """
1496
+ Checks whether a Hesita `Cycle` node meets the requirements of a specific tillage `IpccManagementCategory`.
1497
+
1498
+ Parameters
1499
+ ----------
1500
+ cycle : dict
1501
+ A Hestia `Cycle` node, see: https://www.hestia.earth/schema/Cycle.
1502
+ key : IpccManagementCategory
1503
+ The `IpccManagementCategory` to match.
1504
+
1505
+ Returns
1506
+ -------
1507
+ bool
1508
+ Whether or not the cycle meets the requirements for the category.
1509
+ """
1510
+ LOOKUP = _LOOKUPS["tillage"]
1511
+ target_lookup_values = IPCC_MANAGEMENT_CATEGORY_TO_TILLAGE_MANAGEMENT_LOOKUP_VALUE.get(key, None)
1512
+
1513
+ practices = cycle.get("practices", [])
1514
+ tillage_nodes = filter_list_term_type(
1515
+ practices, [TermTermType.TILLAGE]
1516
+ )
1517
+
1518
+ return cumulative_nodes_lookup_match(
1519
+ tillage_nodes,
1520
+ lookup=LOOKUP,
1521
+ target_lookup_values=target_lookup_values,
1522
+ cumulative_threshold=MIN_AREA_THRESHOLD
1523
+ ) and (
1524
+ key is not IpccManagementCategory.NO_TILLAGE
1525
+ or _check_zero_tillages(tillage_nodes)
1526
+ )
1527
+
1528
+
1529
+ def _check_zero_tillages(practices: list[dict]) -> bool:
1530
+ """
1531
+ Checks whether a list of `Practice`s nodes describe 0 total tillages, or not.
1532
+
1533
+ Parameters
1534
+ ----------
1535
+ practices : list[dict]
1536
+ A list of Hestia `Practice` nodes, see: https://www.hestia.earth/schema/Practice.
1537
+
1538
+ Returns
1539
+ -------
1540
+ bool
1541
+ Whether or not 0 tillages counted.
1542
+ """
1543
+ practice = find_term_match(practices, _NUMBER_OF_TILLAGES_TERM_ID)
1544
+ nTillages = list_sum(practice.get("value", []))
1545
+ return nTillages <= 0
1546
+
1547
+
1548
+ def _get_grouped_is_paddy_rice(grouped_cycles: dict) -> dict:
1549
+ return {
1550
+ year: {
1551
+ _InventoryKey.IS_PADDY_RICE: _check_is_paddy_rice(cycles)
1552
+ } for year, cycles in grouped_cycles.items()
1553
+ }
1554
+
1555
+
1556
+ def _check_is_paddy_rice(cycles: list[dict]) -> bool:
1557
+ LOOKUP = _LOOKUPS["crop"]
1558
+ TARGET_LOOKUP_VALUES = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE.get(
1559
+ IpccLandUseCategory.PADDY_RICE_CULTIVATION, None
1560
+ )
1561
+
1562
+ has_paddy_rice_products = any(cumulative_nodes_lookup_match(
1563
+ filter_list_term_type(
1564
+ cycle.get("products", []) + cycle.get("practices", []),
1565
+ [TermTermType.CROP, TermTermType.FORAGE, TermTermType.LANDCOVER]
1566
+ ),
1567
+ lookup=LOOKUP,
1568
+ target_lookup_values=TARGET_LOOKUP_VALUES,
1569
+ cumulative_threshold=MIN_YIELD_THRESHOLD,
1570
+ default_node_value=MIN_YIELD_THRESHOLD
1571
+ ) for cycle in cycles)
1572
+
1573
+ has_upland_rice_products = any(cumulative_nodes_term_match(
1574
+ filter_list_term_type(
1575
+ cycle.get("products", []) + cycle.get("practices", []),
1576
+ [TermTermType.CROP, TermTermType.FORAGE, TermTermType.LANDCOVER]
1577
+ ),
1578
+ target_term_ids=get_upland_rice_crop_terms_with_cache() + get_upland_rice_land_cover_terms_with_cache(),
1579
+ cumulative_threshold=MIN_YIELD_THRESHOLD,
1580
+ default_node_value=MIN_YIELD_THRESHOLD
1581
+ ) for cycle in cycles)
1582
+
1583
+ has_irrigation = any(
1584
+ check_irrigation(filter_list_term_type(cycle.get("practices", []), [TermTermType.WATERREGIME]))
1585
+ for cycle in cycles
1586
+ )
1587
+
1588
+ return has_paddy_rice_products or (has_upland_rice_products and has_irrigation)
1589
+
1590
+
1591
+ def _should_run_inventory(group: dict) -> bool:
1592
+ """
1593
+ Determines whether there is sufficient data in an inventory year to run the tier 2 model.
1594
+
1595
+ 1. Check that the cycle is not for paddy rice.
1596
+ 2. Check if monthly data has a value for each calendar month.
1597
+ 3. Check if all required keys are present.
1598
+
1599
+ Parameters
1600
+ ----------
1601
+ group : dict
1602
+ Dictionary containing information for a specific inventory year.
1603
+
1604
+ Returns
1605
+ -------
1606
+ bool
1607
+ True if the inventory year is valid, False otherwise.
1608
+ """
1609
+ monthly_data_complete = _check_12_months(
1610
+ group,
1611
+ {
1612
+ _InventoryKey.TEMP_MONTHLY,
1613
+ _InventoryKey.PRECIP_MONTHLY,
1614
+ _InventoryKey.PET_MONTHLY,
1615
+ _InventoryKey.IRRIGATED_MONTHLY
1616
+ }
1617
+ )
1618
+
1619
+ carbon_input_data_complete = all([
1620
+ group.get(_InventoryKey.CARBON_INPUT, 0) > 0,
1621
+ group.get(_InventoryKey.N_CONTENT, 0) > 0,
1622
+ group.get(_InventoryKey.LIGNIN_CONTENT, 0) > 0,
1623
+ ])
1624
+
1625
+ return all([
1626
+ not group.get(_InventoryKey.IS_PADDY_RICE),
1627
+ monthly_data_complete,
1628
+ carbon_input_data_complete,
1629
+ all(key in group.keys() for key in _REQUIRED_KEYS),
1630
+ ])