hestia-earth-models 0.64.8__py3-none-any.whl → 0.64.10__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 (105) hide show
  1. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +175 -0
  2. hestia_earth/models/cml2001Baseline/abioticResourceDepletionMineralsAndMetals.py +136 -0
  3. hestia_earth/models/cycle/siteArea.py +2 -1
  4. hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandOccupation.py +73 -82
  5. hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandTransformation.py +102 -116
  6. hestia_earth/models/environmentalFootprintV3/soilQualityIndexTotalLandUseEffects.py +27 -16
  7. hestia_earth/models/faostat2018/landTransformationFromCropland100YearAverage.py +3 -2
  8. hestia_earth/models/faostat2018/landTransformationFromCropland20YearAverage.py +3 -2
  9. hestia_earth/models/frischknechtEtAl2000/ionisingRadiationKbqU235Eq.py +69 -37
  10. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +31 -243
  11. hestia_earth/models/ipcc2019/animal/fatContent.py +38 -0
  12. hestia_earth/models/ipcc2019/animal/liveweightGain.py +3 -54
  13. hestia_earth/models/ipcc2019/animal/liveweightPerHead.py +3 -54
  14. hestia_earth/models/ipcc2019/animal/pregnancyRateTotal.py +38 -0
  15. hestia_earth/models/ipcc2019/animal/trueProteinContent.py +38 -0
  16. hestia_earth/models/ipcc2019/animal/utils.py +87 -3
  17. hestia_earth/models/ipcc2019/animal/weightAtMaturity.py +4 -10
  18. hestia_earth/models/ipcc2019/belowGroundBiomass.py +529 -0
  19. hestia_earth/models/ipcc2019/biomass_utils.py +406 -0
  20. hestia_earth/models/ipcc2019/{co2ToAirAboveGroundBiomassStockChangeLandUseChange.py → co2ToAirAboveGroundBiomassStockChange.py} +19 -7
  21. hestia_earth/models/ipcc2019/{co2ToAirBelowGroundBiomassStockChangeLandUseChange.py → co2ToAirBelowGroundBiomassStockChange.py} +19 -7
  22. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +402 -73
  23. hestia_earth/models/ipcc2019/{co2ToAirSoilOrganicCarbonStockChangeManagementChange.py → co2ToAirSoilOrganicCarbonStockChange.py} +20 -8
  24. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +3 -1
  25. hestia_earth/models/ipcc2019/pastureGrass_utils.py +6 -7
  26. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  27. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  28. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
  29. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  30. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthWaterStress.py +2 -2
  31. hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  32. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  33. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  34. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  35. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  36. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
  37. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  38. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
  39. hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  40. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  41. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  42. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  43. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  44. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthParticulateMatterFormation.py +2 -2
  45. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  46. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthWaterStress.py +2 -2
  47. hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  48. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  49. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  50. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEutrophication.py +2 -2
  51. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsWaterStress.py +2 -2
  52. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthParticulateMatterFormation.py +2 -2
  53. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthPhotochemicalOzoneFormation.py +2 -2
  54. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthWaterStress.py +2 -2
  55. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsMarineEutrophication.py +2 -2
  56. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsPhotochemicalOzoneFormation.py +2 -2
  57. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialAcidification.py +2 -2
  58. hestia_earth/models/mocking/build_mock_search.py +44 -0
  59. hestia_earth/models/mocking/mock_search.py +8 -49
  60. hestia_earth/models/mocking/search-results.json +3078 -575
  61. hestia_earth/models/poschEtAl2008/terrestrialAcidificationPotentialAccumulatedExceedance.py +6 -3
  62. hestia_earth/models/poschEtAl2008/terrestrialEutrophicationPotentialAccumulatedExceedance.py +6 -3
  63. hestia_earth/models/preload_requests.py +1 -1
  64. hestia_earth/models/schmidt2007/utils.py +13 -4
  65. hestia_earth/models/utils/__init__.py +5 -4
  66. hestia_earth/models/utils/blank_node.py +73 -3
  67. hestia_earth/models/utils/constant.py +8 -1
  68. hestia_earth/models/utils/cycle.py +10 -13
  69. hestia_earth/models/utils/fuel.py +1 -1
  70. hestia_earth/models/utils/impact_assessment.py +39 -15
  71. hestia_earth/models/utils/lookup.py +36 -7
  72. hestia_earth/models/utils/pesticideAI.py +1 -1
  73. hestia_earth/models/utils/property.py +11 -4
  74. hestia_earth/models/utils/term.py +15 -8
  75. hestia_earth/models/version.py +1 -1
  76. {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/METADATA +2 -2
  77. {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/RECORD +103 -90
  78. {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/WHEEL +1 -1
  79. tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +196 -0
  80. tests/models/cml2001Baseline/test_abioticResourceDepletionMineralsAndMetals.py +124 -0
  81. tests/models/edip2003/test_ozoneDepletionPotential.py +1 -13
  82. tests/models/environmentalFootprintV3/test_soilQualityIndexLandOccupation.py +97 -66
  83. tests/models/environmentalFootprintV3/test_soilQualityIndexLandTransformation.py +136 -74
  84. tests/models/environmentalFootprintV3/test_soilQualityIndexTotalLandUseEffects.py +15 -10
  85. tests/models/frischknechtEtAl2000/test_ionisingRadiationKbqU235Eq.py +67 -44
  86. tests/models/impact_assessment/test_emissions.py +1 -0
  87. tests/models/ipcc2019/animal/test_fatContent.py +22 -0
  88. tests/models/ipcc2019/animal/test_liveweightGain.py +4 -2
  89. tests/models/ipcc2019/animal/test_liveweightPerHead.py +4 -2
  90. tests/models/ipcc2019/animal/test_pregnancyRateTotal.py +22 -0
  91. tests/models/ipcc2019/animal/test_trueProteinContent.py +22 -0
  92. tests/models/ipcc2019/animal/test_weightAtMaturity.py +2 -1
  93. tests/models/ipcc2019/test_aboveGroundBiomass.py +27 -63
  94. tests/models/ipcc2019/test_belowGroundBiomass.py +146 -0
  95. tests/models/ipcc2019/test_biomass_utils.py +115 -0
  96. tests/models/ipcc2019/{test_co2ToAirAboveGroundBiomassStockChangeLandUseChange.py → test_co2ToAirAboveGroundBiomassStockChange.py} +5 -5
  97. tests/models/ipcc2019/{test_co2ToAirBelowGroundBiomassStockChangeLandUseChange.py → test_co2ToAirBelowGroundBiomassStockChange.py} +5 -5
  98. tests/models/ipcc2019/{test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py → test_co2ToAirSoilOrganicCarbonStockChange.py} +5 -5
  99. tests/models/ipcc2021/test_gwp100.py +2 -2
  100. tests/models/poschEtAl2008/test_terrestrialAcidificationPotentialAccumulatedExceedance.py +30 -17
  101. tests/models/poschEtAl2008/test_terrestrialEutrophicationPotentialAccumulatedExceedance.py +28 -14
  102. hestia_earth/models/ipcc2019/aboveGroundBiomass_utils.py +0 -180
  103. tests/models/ipcc2019/test_aboveGroundBiomass_utils.py +0 -92
  104. {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/LICENSE +0 -0
  105. {hestia_earth_models-0.64.8.dist-info → hestia_earth_models-0.64.10.dist-info}/top_level.txt +0 -0
@@ -10,12 +10,13 @@ from itertools import product
10
10
  from numpy import array, random, mean
11
11
  from numpy.typing import NDArray
12
12
  from pydash.objects import merge
13
- from typing import Callable, NamedTuple, Optional, Union
13
+ from typing import Any, Callable, NamedTuple, Optional, Union
14
14
 
15
15
  from hestia_earth.schema import (
16
- EmissionMethodTier, EmissionStatsDefinition, MeasurementMethodClassification
16
+ EmissionMethodTier, EmissionStatsDefinition, MeasurementMethodClassification, TermTermType
17
17
  )
18
18
  from hestia_earth.utils.date import diff_in_days, YEAR
19
+ from hestia_earth.utils.model import filter_list_term_type
19
20
  from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
20
21
 
21
22
  from hestia_earth.models.log import log_as_table
@@ -44,10 +45,12 @@ _MAX_CORRELATION = 1
44
45
  _MIN_CORRELATION = 0.5
45
46
  _NOMINAL_ERROR = 75
46
47
  """
47
- carbon stock measurements without an associated `sd` should be assigned a nominal error of 75% (2*sd as a percentage of
48
+ Carbon stock measurements without an associated `sd` should be assigned a nominal error of 75% (2*sd as a percentage of
48
49
  the mean).
49
50
  """
50
- _TRANSITION_PERIOD = 20 * YEAR # 20 years in days
51
+ _DEFAULT_YEARS_SINCE_LUC_EVENT = 999
52
+ _TRANSITION_PERIOD_YEARS = 20
53
+ _TRANSITION_PERIOD_DAYS = 20 * YEAR # 20 years in days
51
54
  _VALID_DATE_FORMATS = {
52
55
  DatestrFormat.YEAR,
53
56
  DatestrFormat.YEAR_MONTH,
@@ -91,6 +94,9 @@ class _InventoryKey(Enum):
91
94
  CARBON_STOCK_CHANGE = "carbon-stock-change"
92
95
  CO2_EMISSION = "carbon-emission"
93
96
  SHARE_OF_EMISSION = "share-of-emissions"
97
+ LAND_USE_SUMMARY = "land-use-summary"
98
+ LAND_USE_CHANGE_EVENT = "luc-event"
99
+ YEARS_SINCE_LUC_EVENT = "years-since-luc-event"
94
100
 
95
101
 
96
102
  CarbonStock = NamedTuple("CarbonStock", [
@@ -319,9 +325,12 @@ def _add_carbon_stock_change_emissions(
319
325
 
320
326
 
321
327
  def create_should_run_function(
328
+ *,
322
329
  carbon_stock_term_id: str,
323
330
  should_compile_inventory_func: Callable[[dict, list[dict], list[dict]], tuple[bool, dict]],
324
- should_run_measurement_func: Callable[[dict], bool] = lambda _: True,
331
+ summarise_land_use_func: Callable[[list[dict]], Any],
332
+ detect_land_use_change_func: Callable[[Any, Any], bool],
333
+ should_run_measurement_func: Callable[[dict], bool] = lambda *_: True,
325
334
  measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING
326
335
  ) -> Callable[[dict], tuple[bool, str, dict, dict]]:
327
336
  """
@@ -342,6 +351,15 @@ def create_should_run_function(
342
351
  `(site: dict, cycles: list[dict], carbon_stock_measurements: list[dict]) -> (should_run: bool, logs: dict)`, to
343
352
  determine whether there is enough site and cycles data available to compile the carbon stock change inventory.
344
353
 
354
+ summarise_land_use_func: Callable[[list[dict]], Any]
355
+ A function with the signature `(nodes: list[dict]) -> Any`, to reduce a list of `landCover`
356
+ [Management](https://www.hestia.earth/schema/Management) nodes into a land use summary that can be compared
357
+ with other summaries to determine whether land use change events have occured.
358
+
359
+ detect_land_use_change_func: Callable[[Any, Any], bool]
360
+ A function with the signature `(summary_a: Any, summary_b: Any) -> bool`, to determine whether a land use
361
+ change event has occured.
362
+
345
363
  should_run_measurement_func : Callable[[dict], bool], optional.
346
364
  An optional measurement validation function, with the signature `(measurement: dict) -> bool`, that can be used
347
365
  to add in additional criteria (`depthUpper`, `depthLower`, etc.) for the inclusion of a measurement in the
@@ -396,6 +414,8 @@ def create_should_run_function(
396
414
  ])
397
415
  ]
398
416
 
417
+ land_cover_nodes = filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
418
+
399
419
  seed = gen_seed(site) # All cycles linked to the same site should be consistent
400
420
  rng = random.default_rng(seed)
401
421
 
@@ -403,14 +423,20 @@ def create_should_run_function(
403
423
  site, cycles, carbon_stock_measurements
404
424
  )
405
425
 
426
+ compile_inventory_func = _create_compile_inventory_function(
427
+ summarise_land_use_func=summarise_land_use_func,
428
+ detect_land_use_change_func=detect_land_use_change_func,
429
+ iterations=_ITERATIONS,
430
+ seed=rng,
431
+ measurement_method_ranking=measurement_method_ranking
432
+ )
433
+
406
434
  inventory, inventory_logs = (
407
- _compile_inventory(
435
+ compile_inventory_func(
408
436
  cycle_id,
409
437
  cycles,
410
438
  carbon_stock_measurements,
411
- iterations=_ITERATIONS,
412
- seed=rng,
413
- measurement_method_ranking=measurement_method_ranking
439
+ land_cover_nodes
414
440
  ) if should_compile_inventory else ({}, {})
415
441
  )
416
442
 
@@ -462,69 +488,126 @@ def _get_site(cycle: dict) -> dict:
462
488
  return cycle.get("site", {})
463
489
 
464
490
 
465
- def _compile_inventory(
466
- cycle_id: str,
467
- cycles: list[dict],
468
- carbon_stock_measurements: list[dict],
491
+ def _create_compile_inventory_function(
492
+ *,
493
+ summarise_land_use_func: Callable[[list[dict]], Any],
494
+ detect_land_use_change_func: Callable[[Any, Any], bool],
469
495
  iterations: int = 10000,
470
496
  seed: Union[int, random.Generator, None] = None,
471
497
  measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING
472
- ) -> tuple[dict, dict]:
498
+ ) -> Callable:
473
499
  """
474
- Compile an annual inventory of carbon stocks, changes in carbon stocks, carbon stock change emissions, and the share
475
- of emissions of cycles based on the provided cycles and measurement data.
500
+ Create a compile inventory function for an emissions from carbon stock change model.
476
501
 
477
- A separate inventory is compiled for each valid `MeasurementMethodClassification` present in the data, and the
478
- strongest available method is chosen for each relevant inventory year. These inventories are then merged into one
479
- final result.
480
-
481
- The final inventory structure is:
482
- ```
483
- {
484
- year (int): {
485
- _InventoryKey.CARBON_STOCK: value (CarbonStock),
486
- _InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
487
- _InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission),
488
- _InventoryKey.SHARE_OF_EMISSION: {
489
- cycle_id (str): value (float),
490
- ...cycle_ids
491
- }
492
- },
493
- ...years
494
- }
495
- ```
502
+ Model-specific validation functions should be passed as parameters to this higher order function to determine how
503
+ land cover data is reduced down to a land use summary and how those land use summaries should be compared to
504
+ determine when land use change events have occured.
496
505
 
497
506
  Parameters
498
507
  ----------
499
- cycle_id : str
500
- The unique identifier of the cycle being processed.
501
- cycles : list[dict]
502
- A list of cycle data dictionaries, each representing land management events or cycles, grouped by years.
503
- carbon_stock_measurements: list[dict]
504
- A list of dictionaries, each representing carbon stock measurements across time and methods.
508
+ summarise_land_use_func: Callable[[list[dict]], Any]
509
+ A function with the signature `(nodes: list[dict]) -> Any`, to reduce a list of `landCover`
510
+ [Management](https://www.hestia.earth/schema/Management) nodes into a land use summary that can be compared
511
+ with other summaries to determine whether land use change events have occured.
512
+
513
+ detect_land_use_change_func: Callable[[Any, Any], bool]
514
+ A function with the signature `(summary_a: Any, summary_b: Any) -> bool`, to determine whether a land use
515
+ change event has occured.
516
+
505
517
  iterations : int, optional
506
518
  The number of iterations for stochastic processing (default is 10,000).
519
+
507
520
  seed : int, random.Generator, or None, optional
508
521
  Seed for random number generation to ensure reproducibility. Default is None.
509
522
 
523
+ measurement_method_ranking : list[MeasurementMethodClassification], optional
524
+ The order in which to prioritise `MeasurementMethodClassification`s when reducing the inventory down to a
525
+ single method per year. Defaults to:
526
+ ```
527
+ MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
528
+ MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
529
+ MeasurementMethodClassification.TIER_3_MODEL,
530
+ MeasurementMethodClassification.TIER_2_MODEL,
531
+ MeasurementMethodClassification.TIER_1_MODEL
532
+ ```
533
+ n.b., measurements with methods not included in the ranking will not be included in the inventory.
510
534
 
511
535
  Returns
512
- -------
513
- tuple[dict, dict]
514
- `(inventory, logs)`
536
+ ----------
537
+ Callable
538
+ The `compile_inventory` function.
515
539
  """
516
- cycle_inventory = _compile_cycle_inventory(cycles)
517
- carbon_stock_inventory = _compile_carbon_stock_inventory(
518
- carbon_stock_measurements, iterations=iterations, seed=seed
519
- )
540
+ def compile_inventory(
541
+ cycle_id: str,
542
+ cycles: list[dict],
543
+ carbon_stock_measurements: list[dict],
544
+ land_cover_nodes: list[dict]
545
+ ) -> tuple[dict, dict]:
546
+ """
547
+ Compile an annual inventory of carbon stocks, changes in carbon stocks, carbon stock change emissions, and the
548
+ share of emissions of cycles based on the provided cycles and measurement data.
520
549
 
521
- logs = _generate_logs(cycle_inventory, carbon_stock_inventory)
550
+ A separate inventory is compiled for each valid `MeasurementMethodClassification` present in the data, and the
551
+ strongest available method is chosen for each relevant inventory year. These inventories are then merged into
552
+ one final result.
553
+
554
+ The final inventory structure is:
555
+ ```
556
+ {
557
+ year (int): {
558
+ _InventoryKey.CARBON_STOCK: value (CarbonStock),
559
+ _InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
560
+ _InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission),
561
+ _InventoryKey.SHARE_OF_EMISSION: {
562
+ cycle_id (str): value (float),
563
+ ...cycle_ids
564
+ },
565
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: value (int)
566
+ },
567
+ ...years
568
+ }
569
+ ```
570
+
571
+ Parameters
572
+ ----------
573
+ cycle_id : str
574
+ The unique identifier of the cycle being processed.
575
+ cycles : list[dict]
576
+ A list of [Cycle](https://www.hestia.earth/schema/Cycles) nodes related to the site.
577
+ carbon_stock_measurements : list[dict]
578
+ A list of [Measurement](https://www.hestia.earth/schema/Measurement) nodes, representing carbon stock
579
+ measurements across time and methods.
580
+ land_cover_nodes : list[dict]
581
+ A list of `landCover `[Management](https://www.hestia.earth/schema/Management) nodes, representing the
582
+ site's land cover over time.
522
583
 
523
- inventory = _squash_inventory(
524
- cycle_id, cycle_inventory, carbon_stock_inventory, measurement_method_ranking=measurement_method_ranking
525
- )
526
584
 
527
- return inventory, logs
585
+ Returns
586
+ -------
587
+ tuple[dict, dict]
588
+ `(inventory, logs)`
589
+ """
590
+ cycle_inventory = _compile_cycle_inventory(cycles)
591
+ carbon_stock_inventory = _compile_carbon_stock_inventory(
592
+ carbon_stock_measurements, iterations=iterations, seed=seed
593
+ )
594
+ land_use_inventory = _compile_land_use_inventory(
595
+ land_cover_nodes, summarise_land_use_func, detect_land_use_change_func
596
+ )
597
+
598
+ inventory = _squash_inventory(
599
+ cycle_id,
600
+ cycle_inventory,
601
+ carbon_stock_inventory,
602
+ land_use_inventory,
603
+ measurement_method_ranking=measurement_method_ranking
604
+ )
605
+
606
+ logs = _generate_logs(cycle_inventory, carbon_stock_inventory, land_use_inventory)
607
+
608
+ return inventory, logs
609
+
610
+ return compile_inventory
528
611
 
529
612
 
530
613
  def _compile_cycle_inventory(cycles: list[dict]) -> dict:
@@ -724,7 +807,7 @@ def _preprocess_carbon_stocks(
724
807
  dates,
725
808
  decay_fn=lambda dt: exponential_decay(
726
809
  dt,
727
- tau=calc_tau(_TRANSITION_PERIOD),
810
+ tau=calc_tau(_TRANSITION_PERIOD_DAYS),
728
811
  initial_value=_MAX_CORRELATION,
729
812
  final_value=_MIN_CORRELATION
730
813
  )
@@ -846,6 +929,100 @@ def _calculate_co2_emissions(carbon_stock_changes_by_year: dict) -> dict:
846
929
  }
847
930
 
848
931
 
932
+ def _compile_land_use_inventory(
933
+ land_cover_nodes: list[dict],
934
+ summarise_land_use_func: Callable[[list[dict]], Any],
935
+ detect_land_use_change_func: Callable[[Any, Any], bool]
936
+ ) -> dict:
937
+ """
938
+ Compile an annual inventory of land use data.
939
+
940
+ The returned inventory has the shape:
941
+ ```
942
+ {
943
+ year (int): {
944
+ _InventoryKey.LAND_USE_SUMMARY: value (Any),
945
+ _InventoryKey.LAND_USE_CHANGE_EVENT: value (bool),
946
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: value (int)
947
+ },
948
+ ...years
949
+ }
950
+ ```
951
+
952
+ Parameters
953
+ ----------
954
+ land_cover_nodes : list[dict]
955
+ A list of `landCover` Management nodes, representing the site's land cover over time.
956
+ summarise_land_use_func: Callable[[list[dict]], Any]
957
+ A function with the signature `(nodes: list[dict]) -> Any`, to reduce a list of `landCover` Management nodes
958
+ into a land use summary that can be compared with other summaries to determine whether land use change events
959
+ have occured.
960
+ detect_land_use_change_func: Callable[[Any, Any], bool]
961
+ A function with the signature `(summary_a: Any, summary_b: Any) -> bool`, to determine whether a land use
962
+ change event has occured.
963
+
964
+ Returns
965
+ -------
966
+ dict
967
+ The land use inventory.
968
+ """
969
+ land_cover_nodes_by_year = group_nodes_by_year(land_cover_nodes)
970
+
971
+ def build_inventory_year(result: dict, year_pair: tuple[int, int]) -> dict:
972
+ """
973
+ Build a year of the inventory using the data from `land_cover_nodes_by_year`.
974
+
975
+ Parameters
976
+ ----------
977
+ inventory: dict
978
+ The land cover change portion of the inventory. Must have the same shape as the returned dict.
979
+ year_pair : tuple[int, int]
980
+ A tuple with the shape `(prev_year, current_year)`.
981
+ Returns
982
+ -------
983
+ dict
984
+ The land use inventory.
985
+ """
986
+
987
+ prev_year, current_year = year_pair
988
+ land_cover_nodes = land_cover_nodes_by_year.get(current_year, {})
989
+
990
+ land_use_summary = summarise_land_use_func(land_cover_nodes)
991
+ prev_land_use_summary = result.get(prev_year, {}).get(_InventoryKey.LAND_USE_SUMMARY, {})
992
+
993
+ is_luc_event = detect_land_use_change_func(land_use_summary, prev_land_use_summary)
994
+
995
+ time_delta = current_year - prev_year
996
+ prev_years_since_luc_event = result.get(prev_year, {}).get(_InventoryKey.YEARS_SINCE_LUC_EVENT, 0)
997
+ years_since_luc_event = time_delta if is_luc_event else prev_years_since_luc_event + time_delta
998
+
999
+ update_dict = {
1000
+ current_year: {
1001
+ _InventoryKey.LAND_USE_SUMMARY: land_use_summary,
1002
+ _InventoryKey.LAND_USE_CHANGE_EVENT: is_luc_event,
1003
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: years_since_luc_event,
1004
+ }
1005
+ }
1006
+ return result | update_dict
1007
+
1008
+ should_run = len(land_cover_nodes_by_year) > 0
1009
+ start_year = min(land_cover_nodes_by_year.keys(), default=None)
1010
+
1011
+ return reduce(
1012
+ build_inventory_year,
1013
+ pairwise(land_cover_nodes_by_year.keys()), # Inventory years need data from previous year to be compiled.
1014
+ {
1015
+ start_year: {
1016
+ _InventoryKey.LAND_USE_SUMMARY: summarise_land_use_func(
1017
+ land_cover_nodes_by_year.get(start_year, [])
1018
+ ),
1019
+ _InventoryKey.LAND_USE_CHANGE_EVENT: False,
1020
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: _DEFAULT_YEARS_SINCE_LUC_EVENT
1021
+ }
1022
+ }
1023
+ ) if should_run else {}
1024
+
1025
+
849
1026
  def _sorted_merge(*sources: Union[dict, list[dict]]) -> dict:
850
1027
  """
851
1028
  Merge one or more dictionaries into a single dictionary, ensuring that the keys are sorted in temporal order.
@@ -873,6 +1050,7 @@ def _squash_inventory(
873
1050
  cycle_id: str,
874
1051
  cycle_inventory: dict,
875
1052
  carbon_stock_inventory: dict,
1053
+ land_use_inventory: dict,
876
1054
  measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING
877
1055
  ) -> dict:
878
1056
  """
@@ -884,6 +1062,7 @@ def _squash_inventory(
884
1062
  ----------
885
1063
  cycle_id : str
886
1064
  The unique identifier of the cycle being processed.
1065
+
887
1066
  cycle_inventory : dict
888
1067
  A dictionary representing the share of emissions for each cycle, grouped by year.
889
1068
  Format:
@@ -898,6 +1077,7 @@ def _squash_inventory(
898
1077
  ...more years
899
1078
  }
900
1079
  ```
1080
+
901
1081
  carbon_stock_inventory : dict
902
1082
  A dictionary representing carbon stock and emissions data grouped by measurement method and year.
903
1083
  Format:
@@ -915,6 +1095,32 @@ def _squash_inventory(
915
1095
  }
916
1096
  ```
917
1097
 
1098
+ land_use_inventory : dict
1099
+ A dictionary representing land use and land use change data grouped by year.
1100
+ Format:
1101
+ ```
1102
+ {
1103
+ year (int): {
1104
+ _InventoryKey.LAND_USE_SUMMARY: value (Any),
1105
+ _InventoryKey.LAND_USE_CHANGE_EVENT: value (bool),
1106
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: value (int)
1107
+ },
1108
+ ...years
1109
+ }
1110
+ ```
1111
+
1112
+ measurement_method_ranking : list[MeasurementMethodClassification], optional
1113
+ The order in which to prioritise `MeasurementMethodClassification`s when reducing the inventory down to a
1114
+ single method per year. Defaults to:
1115
+ ```
1116
+ MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
1117
+ MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
1118
+ MeasurementMethodClassification.TIER_3_MODEL,
1119
+ MeasurementMethodClassification.TIER_2_MODEL,
1120
+ MeasurementMethodClassification.TIER_1_MODEL
1121
+ ```
1122
+ n.b., measurements with methods not included in the ranking will not be included in the inventory.
1123
+
918
1124
  Returns
919
1125
  -------
920
1126
  dict
@@ -951,8 +1157,12 @@ def _squash_inventory(
951
1157
  def squash(result: dict, year: int) -> dict:
952
1158
  update_dict = next(
953
1159
  (
954
- {year: reduce(merge, [carbon_stock_inventory[method][year], cycle_inventory[year]], dict())}
955
- for method in measurement_method_ranking if should_run_group(method, year)
1160
+ {
1161
+ year: {
1162
+ **_get_land_use_change_data(year, land_use_inventory),
1163
+ **reduce(merge, [carbon_stock_inventory[method][year], cycle_inventory[year]], dict())
1164
+ }
1165
+ } for method in measurement_method_ranking if should_run_group(method, year)
956
1166
  ),
957
1167
  {}
958
1168
  )
@@ -961,9 +1171,44 @@ def _squash_inventory(
961
1171
  return reduce(squash, inventory_years, dict())
962
1172
 
963
1173
 
964
- def _generate_logs(cycle_inventory: dict, carbon_stock_inventory: dict) -> dict:
1174
+ def _get_land_use_change_data(
1175
+ year: int,
1176
+ land_use_inventory: dict
1177
+ ) -> dict:
1178
+ """
1179
+ Retrieve a value for `_InventoryKey.YEARS_SINCE_LUC_EVENT` for a specific inventory year, or gapfill it from
1180
+ available data.
1181
+
1182
+ If no land use data is available in the inventory, the site is assumed to have a stable land use and all emissions
1183
+ will be allocated to management changes.
965
1184
  """
966
- Generate logs for the compiled inventory, providing details about cycle and carbon inventories.
1185
+ closest_inventory_year = next(
1186
+ (key for key in land_use_inventory.keys() if key >= year), # get the next inventory year
1187
+ min(
1188
+ land_use_inventory.keys(), key=lambda x: abs(x - year), # else the previous
1189
+ default=None # else return `None`
1190
+ )
1191
+ )
1192
+
1193
+ delta_time = closest_inventory_year - year if closest_inventory_year else 0
1194
+ inventory_data = land_use_inventory.get(closest_inventory_year, {})
1195
+
1196
+ years_since_luc_event = (
1197
+ inventory_data.get(_InventoryKey.YEARS_SINCE_LUC_EVENT, _DEFAULT_YEARS_SINCE_LUC_EVENT) - delta_time
1198
+ )
1199
+
1200
+ return {
1201
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: years_since_luc_event
1202
+ }
1203
+
1204
+
1205
+ def _generate_logs(
1206
+ cycle_inventory: dict,
1207
+ carbon_stock_inventory: dict,
1208
+ land_use_inventory: dict
1209
+ ) -> dict:
1210
+ """
1211
+ Generate logs for the compiled inventory, providing details about cycle, carbon and land use inventories.
967
1212
 
968
1213
  Parameters
969
1214
  ----------
@@ -971,6 +1216,8 @@ def _generate_logs(cycle_inventory: dict, carbon_stock_inventory: dict) -> dict:
971
1216
  The compiled cycle inventory.
972
1217
  carbon_stock_inventory : dict
973
1218
  The compiled carbon stock inventory.
1219
+ land_use_inventory : dict
1220
+ The compiled carbon stock inventory.
974
1221
 
975
1222
  Returns
976
1223
  -------
@@ -980,6 +1227,7 @@ def _generate_logs(cycle_inventory: dict, carbon_stock_inventory: dict) -> dict:
980
1227
  logs = {
981
1228
  "cycle_inventory": _format_cycle_inventory(cycle_inventory),
982
1229
  "carbon_stock_inventory": _format_carbon_stock_inventory(carbon_stock_inventory),
1230
+ "land_use_inventory": _format_land_use_inventory(land_use_inventory)
983
1231
  }
984
1232
  return logs
985
1233
 
@@ -1038,6 +1286,43 @@ def _format_carbon_stock_inventory(carbon_stock_inventory: dict) -> str:
1038
1286
  ) if should_run else "None"
1039
1287
 
1040
1288
 
1289
+ def _format_land_use_inventory(land_use_inventory: dict) -> str:
1290
+ """
1291
+ Format the carbon stock inventory for logging as a table. Rows represent inventory years, columns represent land
1292
+ use change data. If the inventory is invalid, return `"None"` as a string.
1293
+
1294
+ TODO: Implement logging of land use summary.
1295
+ """
1296
+ KEYS = [
1297
+ _InventoryKey.LAND_USE_CHANGE_EVENT,
1298
+ _InventoryKey.YEARS_SINCE_LUC_EVENT
1299
+ ]
1300
+
1301
+ inventory_years = sorted(set(non_empty_list(years for years in land_use_inventory.keys())))
1302
+ should_run = land_use_inventory and len(inventory_years) > 0
1303
+
1304
+ return log_as_table(
1305
+ {
1306
+ "year": year,
1307
+ **{
1308
+ key.value: _LAND_USE_INVENTORY_KEY_TO_FORMAT_FUNC[key](
1309
+ land_use_inventory.get(year, {}).get(key)
1310
+ ) for key in KEYS
1311
+ }
1312
+ } for year in inventory_years
1313
+ ) if should_run else "None"
1314
+
1315
+
1316
+ def _format_bool(value: Optional[bool]) -> str:
1317
+ """Format a bool for logging in a table."""
1318
+ return str(value) if isinstance(value, bool) else "None"
1319
+
1320
+
1321
+ def _format_int(value: Optional[float]) -> str:
1322
+ """Format an int for logging in a table. If the value is invalid, return `"None"` as a string."""
1323
+ return f"{value:.0f}" if isinstance(value, (float, int)) else "None"
1324
+
1325
+
1041
1326
  def _format_number(value: Optional[float]) -> str:
1042
1327
  """Format a float for logging in a table. If the value is invalid, return `"None"` as a string."""
1043
1328
  return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
@@ -1067,8 +1352,20 @@ def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, Ca
1067
1352
  )
1068
1353
 
1069
1354
 
1355
+ _LAND_USE_INVENTORY_KEY_TO_FORMAT_FUNC = {
1356
+ _InventoryKey.LAND_USE_CHANGE_EVENT: _format_bool,
1357
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: _format_int
1358
+ }
1359
+ """
1360
+ Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
1361
+ the `dict` keys.
1362
+ """
1363
+
1364
+
1070
1365
  def create_run_function(
1071
- new_emission_func: Callable[[EmissionMethodTier, dict], dict]
1366
+ new_emission_func: Callable[[EmissionMethodTier, dict], dict],
1367
+ land_use_change_emission_term_id: str,
1368
+ management_change_emission_term_id: str
1072
1369
  ) -> Callable[[str, dict], list[dict]]:
1073
1370
  """
1074
1371
  Create a run function for an emissions from carbon stock change model.
@@ -1080,12 +1377,42 @@ def create_run_function(
1080
1377
  ----------
1081
1378
  new_emission_func : Callable[[EmissionMethodTier, tuple], dict]
1082
1379
  A function, with the signature `(method_tier: dict, **kwargs: dict) -> (emission_node: dict)`.
1380
+ land_use_change_emission_term_id : str
1381
+ The term id for emissions allocated to land use changes.
1382
+ management_change_emission_term_id : str
1383
+ The term id for emissions allocated to management changes.
1083
1384
 
1084
1385
  Returns
1085
1386
  -------
1086
1387
  Callable[[str, dict], list[dict]]
1087
1388
  The customised `run` function with the signature `(cycle_id: str, inventory: dict) -> emissions: list[dict]`.
1088
1389
  """
1390
+ def reduce_emissions(result: dict, year: int, cycle_id: str, inventory: dict):
1391
+ """
1392
+ Assign emissions to either the land use or management change term ids and sum together.
1393
+ """
1394
+ data = inventory[year]
1395
+ years_since_luc_event = data[_InventoryKey.YEARS_SINCE_LUC_EVENT]
1396
+ emission_term_id = (
1397
+ land_use_change_emission_term_id if years_since_luc_event <= _TRANSITION_PERIOD_YEARS
1398
+ else management_change_emission_term_id
1399
+ )
1400
+
1401
+ rescaled_emission = _rescale_carbon_stock_change_emission(
1402
+ data[_InventoryKey.CO2_EMISSION], data[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
1403
+ )
1404
+
1405
+ previous_emission = result.get(emission_term_id)
1406
+
1407
+ update_dict = {
1408
+ emission_term_id: (
1409
+ _add_carbon_stock_change_emissions(previous_emission, rescaled_emission) if previous_emission
1410
+ else rescaled_emission
1411
+ )
1412
+ }
1413
+
1414
+ return result | update_dict
1415
+
1089
1416
  def run(cycle_id: str, inventory: dict) -> list[dict]:
1090
1417
  """
1091
1418
  Calculate emissions for a specific cycle using from a carbon stock change using pre-compiled inventory data.
@@ -1103,22 +1430,24 @@ def create_run_function(
1103
1430
  Returns
1104
1431
  -------
1105
1432
  list[dict]
1106
- A list of [Emission nodes](https://www.hestia.earth/schema/Emission) containing model results.
1433
+ A list of [Emission](https://www.hestia.earth/schema/Emission) nodes containing model results.
1107
1434
  """
1108
- rescaled_emissions = [
1109
- _rescale_carbon_stock_change_emission(
1110
- group[_InventoryKey.CO2_EMISSION], group[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
1111
- ) for group in inventory.values()
1112
- ]
1113
- total_emission = reduce(_add_carbon_stock_change_emissions, rescaled_emissions)
1114
-
1115
- descriptive_stats = calc_descriptive_stats(
1116
- total_emission.value,
1117
- EmissionStatsDefinition.SIMULATED,
1118
- decimals=6
1435
+ assigned_emissions = reduce(
1436
+ lambda result, year: reduce_emissions(result, year, cycle_id, inventory),
1437
+ inventory.keys(),
1438
+ {}
1119
1439
  )
1120
1440
 
1121
- method_tier = total_emission.method
1122
- return [new_emission_func(method_tier=method_tier, **descriptive_stats)]
1441
+ return [
1442
+ new_emission_func(
1443
+ term_id=emission_term_id,
1444
+ method_tier=total_emission.method,
1445
+ **calc_descriptive_stats(
1446
+ total_emission.value,
1447
+ EmissionStatsDefinition.SIMULATED,
1448
+ decimals=6
1449
+ )
1450
+ ) for emission_term_id, total_emission in assigned_emissions.items()
1451
+ ]
1123
1452
 
1124
1453
  return run