hestia-earth-models 0.70.0__py3-none-any.whl → 0.70.1__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.
Files changed (168) hide show
  1. hestia_earth/models/aware/scarcityWeightedWaterUse.py +8 -16
  2. hestia_earth/models/config/Cycle.json +52 -64
  3. hestia_earth/models/config/ImpactAssessment.json +12 -4
  4. hestia_earth/models/config/Site.json +33 -22
  5. hestia_earth/models/cycle/transformation.py +1 -1
  6. hestia_earth/models/cycle/utils.py +0 -6
  7. hestia_earth/models/data/ecoinventV3/__init__.py +15 -13
  8. hestia_earth/models/ecoalimV9/__init__.py +13 -0
  9. hestia_earth/models/ecoalimV9/cycle.py +128 -0
  10. hestia_earth/models/ecoalimV9/impact_assessment.py +125 -0
  11. hestia_earth/models/ecoalimV9/utils.py +31 -0
  12. hestia_earth/models/ecoinventV3/__init__.py +6 -14
  13. hestia_earth/models/ecoinventV3/utils.py +1 -29
  14. hestia_earth/models/ecoinventV3AndEmberClimate/__init__.py +8 -2
  15. hestia_earth/models/emissionNotRelevant/__init__.py +33 -8
  16. hestia_earth/models/{cycle → hestia}/aboveGroundCropResidue.py +4 -3
  17. hestia_earth/models/{cycle → hestia}/aboveGroundCropResidueTotal.py +1 -1
  18. hestia_earth/models/{site → hestia}/brackishWater.py +1 -1
  19. hestia_earth/models/{site → hestia}/cationExchangeCapacityPerKgSoil.py +1 -1
  20. hestia_earth/models/{cycle → hestia}/coldCarcassWeightPerHead.py +1 -1
  21. hestia_earth/models/{cycle → hestia}/coldDressedCarcassWeightPerHead.py +1 -1
  22. hestia_earth/models/{cycle → hestia}/concentrateFeed.py +1 -1
  23. hestia_earth/models/{cycle → hestia}/cropResidueManagement.py +1 -1
  24. hestia_earth/models/{cycle → hestia}/croppingIntensity.py +1 -1
  25. hestia_earth/models/{cycle → hestia}/energyContentLowerHeatingValue.py +1 -1
  26. hestia_earth/models/{cycle → hestia}/excretaKgMass.py +7 -2
  27. hestia_earth/models/{cycle → hestia}/excretaKgN.py +1 -1
  28. hestia_earth/models/{cycle → hestia}/excretaKgVs.py +1 -1
  29. hestia_earth/models/{cycle → hestia}/feedConversionRatio/__init__.py +1 -1
  30. hestia_earth/models/{site → hestia}/flowingWater.py +1 -1
  31. hestia_earth/models/{site → hestia}/freshWater.py +1 -1
  32. hestia_earth/models/{cycle → hestia}/inorganicFertiliser.py +1 -1
  33. hestia_earth/models/{cycle → hestia}/irrigatedTypeUnspecified.py +14 -19
  34. hestia_earth/models/hestia/landCover.py +30 -22
  35. hestia_earth/models/{cycle → hestia}/liveAnimal.py +1 -1
  36. hestia_earth/models/{cycle → hestia}/longFallowRatio.py +1 -1
  37. hestia_earth/models/{cycle → hestia}/materialAndSubstrate.py +1 -1
  38. hestia_earth/models/{cycle → hestia}/milkYield.py +1 -1
  39. hestia_earth/models/{site → hestia}/netPrimaryProduction.py +1 -1
  40. hestia_earth/models/{site → hestia}/organicCarbonPerHa.py +1 -1
  41. hestia_earth/models/{cycle → hestia}/pastureGrass.py +1 -1
  42. hestia_earth/models/{cycle → hestia}/pastureSystem.py +1 -1
  43. hestia_earth/models/{site → hestia}/potentialEvapotranspirationAnnual.py +3 -3
  44. hestia_earth/models/{site → hestia}/potentialEvapotranspirationMonthly.py +3 -3
  45. hestia_earth/models/{site → hestia}/precipitationAnnual.py +3 -3
  46. hestia_earth/models/{site → hestia}/precipitationMonthly.py +3 -3
  47. hestia_earth/models/{site → hestia}/rainfallAnnual.py +3 -3
  48. hestia_earth/models/{site → hestia}/rainfallMonthly.py +3 -3
  49. hestia_earth/models/{cycle → hestia}/readyToCookWeightPerHead.py +1 -1
  50. hestia_earth/models/{cycle → hestia}/residueBurnt.py +1 -1
  51. hestia_earth/models/{cycle → hestia}/residueIncorporated.py +1 -1
  52. hestia_earth/models/{cycle → hestia}/residueLeftOnField.py +1 -1
  53. hestia_earth/models/hestia/residueRemoved.py +65 -13
  54. hestia_earth/models/{site → hestia}/salineWater.py +1 -1
  55. hestia_earth/models/{site → hestia}/soilMeasurement.py +1 -1
  56. hestia_earth/models/{cycle → hestia}/stockingDensityAnimalHousingAverage.py +1 -1
  57. hestia_earth/models/{site → hestia}/temperatureAnnual.py +3 -3
  58. hestia_earth/models/{site → hestia}/temperatureMonthly.py +3 -3
  59. hestia_earth/models/{site → hestia}/totalNitrogenPerKgSoil.py +1 -1
  60. hestia_earth/models/{cycle → hestia}/unknownPreSeasonWaterRegime.py +1 -1
  61. hestia_earth/models/hestia/utils.py +93 -0
  62. hestia_earth/models/{site → hestia}/waterDepth.py +1 -1
  63. hestia_earth/models/hestia/waterSalinity.py +78 -0
  64. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +1 -1
  65. hestia_earth/models/ipcc2019/belowGroundBiomass.py +1 -1
  66. hestia_earth/models/ipcc2019/biomass_utils.py +2 -4
  67. hestia_earth/models/ipcc2019/ch4ToAirFloodedRice.py +163 -78
  68. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +31 -20
  69. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +16 -9
  70. hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +35 -47
  71. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1.py +86 -1
  72. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2.py +127 -1
  73. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +7 -5
  74. hestia_earth/models/mocking/search-results.json +764 -772
  75. hestia_earth/models/pooreNemecek2018/utils.py +8 -2
  76. hestia_earth/models/schmidt2007/ch4ToAirWasteTreatment.py +1 -4
  77. hestia_earth/models/schmidt2007/h2SToAirWasteTreatment.py +1 -4
  78. hestia_earth/models/schmidt2007/n2OToAirWasteTreatmentDirect.py +1 -4
  79. hestia_earth/models/schmidt2007/nh3ToAirWasteTreatment.py +1 -4
  80. hestia_earth/models/utils/background_emissions.py +52 -0
  81. hestia_earth/models/utils/blank_node.py +9 -5
  82. hestia_earth/models/utils/impact_assessment.py +26 -17
  83. hestia_earth/models/utils/lookup.py +48 -39
  84. hestia_earth/models/utils/measurement.py +3 -3
  85. hestia_earth/models/version.py +1 -1
  86. {hestia_earth_models-0.70.0.dist-info → hestia_earth_models-0.70.1.dist-info}/METADATA +2 -2
  87. {hestia_earth_models-0.70.0.dist-info → hestia_earth_models-0.70.1.dist-info}/RECORD +163 -158
  88. tests/models/aware/test_scarcityWeightedWaterUse.py +1 -12
  89. tests/models/ecoalimV9/__init__.py +0 -0
  90. tests/models/ecoalimV9/test_cycle.py +21 -0
  91. tests/models/ecoalimV9/test_impact_assessment.py +24 -0
  92. tests/models/environmentalFootprintV3_1/test_scarcityWeightedWaterUse.py +4 -2
  93. tests/models/{cycle → hestia}/test_aboveGroundCropResidue.py +1 -1
  94. tests/models/{cycle → hestia}/test_aboveGroundCropResidueTotal.py +1 -1
  95. tests/models/{site → hestia}/test_brackishWater.py +1 -1
  96. tests/models/{site → hestia}/test_cationExchangeCapacityPerKgSoil.py +1 -1
  97. tests/models/{cycle → hestia}/test_coldCarcassWeightPerHead.py +1 -1
  98. tests/models/{cycle → hestia}/test_coldDressedCarcassWeightPerHead.py +1 -1
  99. tests/models/{cycle → hestia}/test_concentrateFeed.py +1 -1
  100. tests/models/{cycle → hestia}/test_cropResidueManagement.py +1 -1
  101. tests/models/{cycle → hestia}/test_croppingIntensity.py +1 -1
  102. tests/models/{cycle → hestia}/test_energyContentLowerHeatingValue.py +5 -3
  103. tests/models/{cycle → hestia}/test_excretaKgMass.py +1 -1
  104. tests/models/{cycle → hestia}/test_excretaKgN.py +1 -1
  105. tests/models/{cycle → hestia}/test_excretaKgVs.py +1 -1
  106. tests/models/{cycle → hestia}/test_feedConversionRatio.py +1 -1
  107. tests/models/{site → hestia}/test_flowingWater.py +1 -1
  108. tests/models/{site → hestia}/test_freshWater.py +1 -1
  109. tests/models/{cycle → hestia}/test_inorganicFertiliser.py +1 -1
  110. tests/models/{cycle → hestia}/test_irrigatedTypeUnspecified.py +2 -5
  111. tests/models/hestia/test_landCover.py +4 -34
  112. tests/models/{cycle → hestia}/test_liveAnimal.py +1 -1
  113. tests/models/{cycle → hestia}/test_longFallowRatio.py +1 -1
  114. tests/models/{site → hestia}/test_management.py +1 -1
  115. tests/models/{cycle → hestia}/test_materialsAndSubstrate.py +1 -1
  116. tests/models/{cycle → hestia}/test_milkYield.py +1 -1
  117. tests/models/{site → hestia}/test_netPrimaryProduction.py +1 -1
  118. tests/models/{site → hestia}/test_organicCarbonPerHa.py +1 -1
  119. tests/models/{site → hestia}/test_organicCarbonPerKgSoil.py +1 -1
  120. tests/models/{site → hestia}/test_organicCarbonPerM3Soil.py +1 -1
  121. tests/models/{site → hestia}/test_organicMatterPerKgSoil.py +1 -1
  122. tests/models/{site → hestia}/test_organicMatterPerM3Soil.py +1 -1
  123. tests/models/{cycle → hestia}/test_pastureGrass.py +1 -1
  124. tests/models/{cycle → hestia}/test_pastureSystem.py +1 -1
  125. tests/models/{site → hestia}/test_potentialEvapotranspirationAnnual.py +1 -1
  126. tests/models/{site → hestia}/test_potentialEvapotranspirationMonthly.py +1 -1
  127. tests/models/{site → hestia}/test_precipitationAnnual.py +1 -1
  128. tests/models/{site → hestia}/test_precipitationMonthly.py +1 -1
  129. tests/models/{site → hestia}/test_rainfallAnnual.py +1 -1
  130. tests/models/{site → hestia}/test_rainfallMonthly.py +1 -1
  131. tests/models/{cycle → hestia}/test_readyToCookWeightPerHead.py +1 -1
  132. tests/models/{cycle → hestia}/test_residueBurnt.py +1 -1
  133. tests/models/{cycle → hestia}/test_residueIncorporated.py +1 -1
  134. tests/models/{cycle → hestia}/test_residueLeftOnField.py +1 -1
  135. tests/models/hestia/test_residueRemoved.py +15 -3
  136. tests/models/{site → hestia}/test_salineWater.py +1 -1
  137. tests/models/{site → hestia}/test_soilMeasurement.py +2 -2
  138. tests/models/{cycle → hestia}/test_stockingDensityAnimalHousingAverage.py +1 -1
  139. tests/models/{site → hestia}/test_temperatureAnnual.py +1 -1
  140. tests/models/{site → hestia}/test_temperatureMonthly.py +1 -1
  141. tests/models/{site → hestia}/test_totalNitrogenPerKgSoil.py +1 -1
  142. tests/models/{cycle → hestia}/test_unknownPreSeasonWaterRegime.py +1 -1
  143. tests/models/{site → hestia}/test_waterDepth.py +1 -1
  144. tests/models/hestia/test_waterSalinity.py +26 -0
  145. tests/models/ipcc2019/test_ch4ToAirFloodedRice.py +10 -42
  146. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +2 -1
  147. tests/models/ipcc2019/test_nonCo2EmissionsToAirNaturalVegetationBurning.py +3 -2
  148. tests/models/test_ecoinventV3AndEmberClimate.py +2 -2
  149. tests/models/test_emissionNotRelevant.py +0 -8
  150. tests/models/utils/test_measurement.py +1 -1
  151. hestia_earth/models/cycle/residueRemoved.py +0 -54
  152. hestia_earth/models/hestia/nh3ToSurfaceWaterAquacultureSystems.py +0 -64
  153. hestia_earth/models/site/utils.py +0 -93
  154. tests/models/cycle/test_residueRemoved.py +0 -37
  155. tests/models/hestia/test_nh3ToSurfaceWaterAquacultureSystems.py +0 -51
  156. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioCarbon.py +0 -0
  157. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioDryMatter.py +0 -0
  158. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioEnergy.py +0 -0
  159. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioFedWeight.py +0 -0
  160. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioNitrogen.py +0 -0
  161. /hestia_earth/models/{site → hestia}/management.py +0 -0
  162. /hestia_earth/models/{site → hestia}/organicCarbonPerKgSoil.py +0 -0
  163. /hestia_earth/models/{site → hestia}/organicCarbonPerM3Soil.py +0 -0
  164. /hestia_earth/models/{site → hestia}/organicMatterPerKgSoil.py +0 -0
  165. /hestia_earth/models/{site → hestia}/organicMatterPerM3Soil.py +0 -0
  166. {hestia_earth_models-0.70.0.dist-info → hestia_earth_models-0.70.1.dist-info}/LICENSE +0 -0
  167. {hestia_earth_models-0.70.0.dist-info → hestia_earth_models-0.70.1.dist-info}/WHEEL +0 -0
  168. {hestia_earth_models-0.70.0.dist-info → hestia_earth_models-0.70.1.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,10 @@
1
+ from functools import reduce
1
2
  from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition, TermTermType
2
3
  from hestia_earth.utils.model import filter_list_term_type, find_term_match
3
- from hestia_earth.utils.tools import list_sum, safe_parse_float
4
+ from hestia_earth.utils.tools import list_sum, safe_parse_float, non_empty_list
4
5
 
5
- from hestia_earth.models.log import debugValues, logRequirements, logShouldRun
6
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table, debugValues
7
+ from hestia_earth.models.utils import multiply_values
6
8
  from hestia_earth.models.utils.term import get_lookup_value
7
9
  from hestia_earth.models.utils.emission import _new_emission
8
10
  from hestia_earth.models.utils.product import has_flooded_rice
@@ -12,18 +14,17 @@ from . import MODEL
12
14
 
13
15
  REQUIREMENTS = {
14
16
  "Cycle": {
15
- "practices": [{"@type": "Practice", "value": "", "term.@id": "croppingDuration"}],
17
+ "practices": [
18
+ {"@type": "Practice", "value": "", "term.@id": "croppingDuration"},
19
+ {"@type": "Practice", "value": "", "term.termType": ["landUseManagement", "waterRegime"]}
20
+ ],
16
21
  "site": {
17
22
  "@type": "Site",
18
23
  "country": {"@type": "Term", "termType": "region"}
19
24
  },
20
25
  "optional": {
21
26
  "inputs": [
22
- {
23
- "@type": "Input",
24
- "value": "",
25
- "term.termType": "organicFertiliser"
26
- },
27
+ {"@type": "Input", "value": "", "term.termType": "organicFertiliser"},
27
28
  {
28
29
  "@type": "Input",
29
30
  "value": "",
@@ -32,28 +33,35 @@ REQUIREMENTS = {
32
33
  }
33
34
  ],
34
35
  "products": [{"@type": "Product", "value": "", "term.@id": "aboveGroundCropResidueIncorporated"}],
35
- "practices": [
36
- {"@type": "Practice", "value": "", "term.termType": "cropResidueManagement"},
37
- {"@type": "Practice", "value": "", "term.termType": "landUseManagement"},
38
- {"@type": "Practice", "value": "", "term.termType": "waterRegime"}
39
- ]
36
+ "practices": [{"@type": "Practice", "value": "", "term.termType": "cropResidueManagement"}]
40
37
  }
41
38
  }
42
39
  }
43
40
  LOOKUPS = {
44
41
  "landUseManagement": [
45
- "IPCC_2019_CH4_rice_SFw", "IPCC_2019_CH4_rice_SFw-min", "IPCC_2019_CH4_rice_SFw-max",
46
- "IPCC_2019_CH4_rice_SFw-sd",
47
- "IPCC_2019_CH4_rice_SFp", "IPCC_2019_CH4_rice_SFp-min", "IPCC_2019_CH4_rice_SFp-max",
42
+ "IPCC_2019_CH4_rice_SFp",
43
+ "IPCC_2019_CH4_rice_SFp-min",
44
+ "IPCC_2019_CH4_rice_SFp-max",
48
45
  "IPCC_2019_CH4_rice_SFp-sd"
49
46
  ],
50
47
  "waterRegime": [
51
- "IPCC_2019_CH4_rice_SFw", "IPCC_2019_CH4_rice_SFw-min", "IPCC_2019_CH4_rice_SFw-max",
52
- "IPCC_2019_CH4_rice_SFw-sd",
53
- "IPCC_2019_CH4_rice_SFp", "IPCC_2019_CH4_rice_SFp-min", "IPCC_2019_CH4_rice_SFp-max",
54
- "IPCC_2019_CH4_rice_SFp-sd"
48
+ "IPCC_2019_CH4_rice_SFw",
49
+ "IPCC_2019_CH4_rice_SFw-min",
50
+ "IPCC_2019_CH4_rice_SFw-max",
51
+ "IPCC_2019_CH4_rice_SFw-sd"
52
+ ],
53
+ "organicFertiliser": [
54
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight",
55
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight_min",
56
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight_max",
57
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight_sd"
58
+ ],
59
+ "cropResidueManagement": [
60
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight",
61
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight_min",
62
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight_max",
63
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight_sd"
55
64
  ],
56
- "organicFertiliser": ["IPCC_2019_CH4_rice_CFOA_kg_fresh_weight", "IPCC_2019_CH4_rice_CFOA_kg_dry_weight"],
57
65
  "region-ch4ef-IPCC2019": ["CH4_ef", "CH4_ef_min", "CH4_ef_max", "CH4_ef_sd"]
58
66
  }
59
67
  RETURNS = {
@@ -68,92 +76,138 @@ RETURNS = {
68
76
  }
69
77
  TERM_ID = 'ch4ToAirFloodedRice'
70
78
  TIER = EmissionMethodTier.TIER_1.value
79
+ _STATS = ['value', 'min', 'max', 'sd']
71
80
 
72
81
 
73
82
  def _emission(value: float, min: float, max: float, sd: float):
74
83
  emission = _new_emission(TERM_ID, MODEL)
75
84
  emission['value'] = [value]
76
- emission['min'] = [min]
77
- emission['max'] = [max]
78
- emission['sd'] = [sd]
85
+ if min is not None:
86
+ emission['min'] = [min]
87
+ if max is not None:
88
+ emission['max'] = [max]
89
+ if sd is not None:
90
+ emission['sd'] = [sd]
79
91
  emission['methodTier'] = TIER
80
92
  emission['statsDefinition'] = EmissionStatsDefinition.MODELLED.value
81
93
  return emission
82
94
 
83
95
 
84
- def _get_CH4_ef(country: str, suffix: str = ''):
96
+ def _get_CH4_ef(country: str, suffix: str = 'value'):
85
97
  lookup_name = 'region-ch4ef-IPCC2019.csv'
98
+ lookup = 'CH4_ef'
99
+ lookup = '_'.join([lookup, suffix]) if suffix != 'value' else lookup
86
100
  return safe_parse_float(
87
- get_region_lookup_value(lookup_name, country, 'CH4_ef' + suffix, model=MODEL, term=TERM_ID)
101
+ get_region_lookup_value(lookup_name, country, lookup, model=MODEL, term=TERM_ID),
102
+ default=None
88
103
  )
89
104
 
90
105
 
91
- def _get_practice_lookup(term: dict, col: str):
92
- return safe_parse_float(get_lookup_value(term, col, model=MODEL, term=TERM_ID))
93
-
94
-
95
- def _get_cropResidue_value(cycle: dict, suffix: str = ''):
106
+ def _get_cropResidue_value(cycle: dict, suffix: str = 'value'):
107
+ product_id = 'aboveGroundCropResidueIncorporated'
96
108
  abgIncorporated = list_sum(
97
- find_term_match(cycle.get('products', []), 'aboveGroundCropResidueIncorporated').get('value', [])
109
+ find_term_match(cycle.get('products', []), product_id).get('value', []),
110
+ default=None
98
111
  )
99
112
  abgManagement = filter_list_term_type(cycle.get('practices', []), TermTermType.CROPRESIDUEMANAGEMENT)
100
113
  term = abgManagement[0].get('term', {}) if len(abgManagement) > 0 else None
114
+ lookup = 'IPCC_2019_CH4_rice_CFOA_kg_dry_weight'
115
+ lookup = '_'.join([lookup, suffix]) if suffix != 'value' else lookup
101
116
  factor = safe_parse_float(
102
- get_lookup_value(term, LOOKUPS['organicFertiliser'][1] + suffix, model=MODEL, term=TERM_ID)
103
- ) if term else 0
104
- return abgIncorporated * factor
117
+ get_lookup_value(term, lookup, model=MODEL, term=TERM_ID),
118
+ default=None
119
+ ) if term else None
105
120
 
121
+ debugValues(cycle, model=MODEL, term=TERM_ID,
122
+ **{'cropResidue_' + suffix: log_as_table({
123
+ 'product-id': product_id,
124
+ 'product-value': abgIncorporated,
125
+ 'factor': factor
126
+ })})
106
127
 
107
- def _get_fertiliser_value(input: dict, suffix: str = ''):
128
+ return multiply_values([abgIncorporated, factor])
129
+
130
+
131
+ def _get_fertiliser_values(input: dict, suffix: str = 'value'):
108
132
  term = input.get('term', {})
133
+ lookup = 'IPCC_2019_CH4_rice_CFOA_kg_fresh_weight'
134
+ lookup = '_'.join([lookup, suffix]) if suffix != 'value' else lookup
109
135
  factor = safe_parse_float(
110
- get_lookup_value(term, LOOKUPS['organicFertiliser'][0] + suffix, model=MODEL, term=TERM_ID)
136
+ get_lookup_value(term, lookup, model=MODEL, term=TERM_ID),
137
+ default=None
111
138
  )
112
- return list_sum(input.get('value', [])) * factor
139
+ value = list_sum(input.get('value', []))
140
+ return {'input-id': term.get('@id'), 'input-value': value, 'factor': factor}
113
141
 
114
142
 
115
- def _calculate_SFo(cycle: dict, suffix: str = ''):
116
- cropResidue = _get_cropResidue_value(cycle, suffix)
117
- fertilisers = get_organicFertiliser_inputs(cycle)
118
- fert_value = list_sum([_get_fertiliser_value(i, suffix) for i in fertilisers])
119
- return (1 + (fert_value/1000) + (cropResidue/1000)) ** 0.59
143
+ def _get_fertiliser_value(cycle: dict, suffix: str = 'value'):
144
+ fertiliser_values = [_get_fertiliser_values(i, suffix) for i in get_organicFertiliser_inputs(cycle)]
120
145
 
146
+ debugValues(cycle, model=MODEL, term=TERM_ID,
147
+ **{'fertiliser_' + suffix: log_as_table(fertiliser_values)})
121
148
 
122
- def _calculate_SF_average(practices: list, factor: str):
123
- values = [
124
- (_get_practice_lookup(p.get('term', {}), factor), list_sum(p.get('value', []), None)) for p in practices
149
+ valid_fertiliser_values = [
150
+ value for value in fertiliser_values
151
+ if all([value.get('input-value') is not None, value.get('factor') is not None])
125
152
  ]
126
- # sum only values that are numbers
127
- return list_sum([factor * percent / 100 for factor, percent in values if percent is not None])
153
+ fert_value = list_sum([
154
+ value.get('input-value') * value.get('factor')
155
+ for value in valid_fertiliser_values
156
+ ])
157
+ return fert_value
128
158
 
129
159
 
130
- def _calculate_factor(cycle: dict, country: str, practices: list, suffix: str = ''):
131
- CH4_ef = _get_CH4_ef(country, suffix)
132
- SFw = _calculate_SF_average(practices, 'IPCC_2019_CH4_rice_SFw' + suffix)
133
- SFp = _calculate_SF_average(practices, 'IPCC_2019_CH4_rice_SFp' + suffix)
134
- SFo = _calculate_SFo(cycle, suffix)
135
- debugValues(cycle, model=MODEL, term=TERM_ID, **{
136
- 'CH4_ef' + suffix: CH4_ef,
137
- 'SFw' + suffix: SFw,
138
- 'SFp' + suffix: SFp,
139
- 'SFo' + suffix: SFo
140
- })
141
- return CH4_ef * (SFw if SFw > 0 else 1) * (SFp if SFp > 0 else 1) * SFo
160
+ def _calculate_SFo(cycle: dict, suffix: str = 'value'):
161
+ cropResidue = _get_cropResidue_value(cycle, suffix)
162
+ fertiliser = _get_fertiliser_value(cycle, suffix)
142
163
 
164
+ return (1 + (fertiliser/1000) + (cropResidue/1000)) ** 0.59
143
165
 
144
- def _get_croppingDuration(croppingDuration: dict, key: str = 'value'):
145
- return list_sum(croppingDuration.get(key, croppingDuration.get('value', [])))
146
166
 
167
+ def _get_practice_values(practice: dict, col: str, default=None):
168
+ term = practice.get('term', {})
169
+ factor = safe_parse_float(get_lookup_value(term, col, model=MODEL, term=TERM_ID), default)
170
+ return {
171
+ 'practice-id': term.get('@id'),
172
+ 'factor': factor,
173
+ 'practice-value': list_sum(practice.get('value', []), default=default)
174
+ } if factor is not None else None
147
175
 
148
- def _run(cycle: dict, croppingDuration: dict, country: str):
149
- practices = filter_list_term_type(cycle.get('practices', []), [
150
- TermTermType.WATERREGIME, TermTermType.LANDUSEMANAGEMENT
151
- ])
152
176
 
153
- value = _calculate_factor(cycle, country, practices) * _get_croppingDuration(croppingDuration)
154
- min = _calculate_factor(cycle, country, practices, '_min') * _get_croppingDuration(croppingDuration, 'min')
155
- max = _calculate_factor(cycle, country, practices, '_max') * _get_croppingDuration(croppingDuration, 'max')
156
- sd = (max-min)/4
177
+ def _calculate_SF_total(cycle: dict, practices: list, lookup: str, suffix: str = 'value', default=None):
178
+ lookup_column = '-'.join([lookup, suffix]) if suffix != 'value' else lookup
179
+ values = non_empty_list([_get_practice_values(p, lookup_column) for p in practices])
180
+
181
+ debugValues(cycle, model=MODEL, term=TERM_ID,
182
+ **{lookup_column: log_as_table(values)})
183
+
184
+ used_values = [value for value in values if value.get('practice-value') is not None]
185
+
186
+ # sum only values that are numbers
187
+ return (
188
+ list_sum([
189
+ value.get('factor') * value.get('practice-value') for value in used_values
190
+ ], default=None) / list_sum([
191
+ value.get('practice-value') for value in used_values
192
+ ])
193
+ ) if used_values else (
194
+ default if suffix == 'value' else None
195
+ )
196
+
197
+
198
+ def _value_from_factors(values: list, key: str = 'value'):
199
+ # get the value from all factors, and only run if all are provided
200
+ all_values = [value.get(key) for value in values]
201
+ return multiply_values(all_values) if all([v is not None for v in all_values]) else None
202
+
203
+
204
+ def _run(values: list):
205
+ value = _value_from_factors(values, 'value')
206
+ min = _value_from_factors(values, 'min')
207
+ max = _value_from_factors(values, 'max')
208
+ sd = _value_from_factors(values, 'sd')
209
+
210
+ sd = (max-min)/4 if all([max, min]) else None
157
211
 
158
212
  return [_emission(value, min, max, sd)]
159
213
 
@@ -162,20 +216,51 @@ def _should_run(cycle: dict):
162
216
  country = cycle.get('site', {}).get('country', {}).get('@id')
163
217
 
164
218
  flooded_rice = has_flooded_rice(cycle.get('products', []))
219
+ practices = cycle.get('practices', [])
165
220
 
166
- croppingDuration = find_term_match(cycle.get('practices', []), 'croppingDuration', None)
221
+ croppingDuration = find_term_match(practices, 'croppingDuration', None)
167
222
  has_croppingDuration = croppingDuration is not None
223
+ croppingDuration = reduce(lambda p, key: p | {
224
+ key: list_sum(croppingDuration.get(key) or [], default=None)
225
+ }, _STATS, {}) if has_croppingDuration else {}
226
+
227
+ CH4_ef = reduce(lambda p, key: p | {key: _get_CH4_ef(country, key)}, _STATS, {})
228
+ SFo = reduce(lambda p, key: p | {key: _calculate_SFo(cycle, key)}, _STATS, {})
229
+
230
+ water_regime = filter_list_term_type(practices, TermTermType.WATERREGIME)
231
+ SFw = reduce(lambda p, key: p | {
232
+ key: _calculate_SF_total(cycle, water_regime, 'IPCC_2019_CH4_rice_SFw', key)
233
+ }, _STATS, {})
234
+
235
+ land_use_management = filter_list_term_type(practices, TermTermType.LANDUSEMANAGEMENT)
236
+ SFp = reduce(lambda p, key: p | {
237
+ key: _calculate_SF_total(cycle, land_use_management, 'IPCC_2019_CH4_rice_SFp', key, default=1)
238
+ }, _STATS, {})
168
239
 
169
240
  logRequirements(cycle, model=MODEL, term=TERM_ID,
170
241
  has_flooded_rice=flooded_rice,
171
- has_croppingDuration=has_croppingDuration,
172
- country=country)
173
-
174
- should_run = all([flooded_rice, has_croppingDuration, country])
242
+ country=country,
243
+ values=log_as_table([
244
+ {'name': 'croppingDuration'} | croppingDuration,
245
+ {'name': 'CH4-ef'} | CH4_ef,
246
+ {'name': 'SFo'} | SFo,
247
+ {'name': 'SFw'} | SFw,
248
+ {'name': 'SFp'} | SFp,
249
+ ]))
250
+
251
+ should_run = all([
252
+ flooded_rice,
253
+ has_croppingDuration,
254
+ country,
255
+ CH4_ef.get('value') is not None,
256
+ SFo.get('value') is not None,
257
+ SFw.get('value') is not None,
258
+ SFp.get('value') is not None,
259
+ ])
175
260
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
176
- return should_run, croppingDuration, country
261
+ return should_run, [croppingDuration, CH4_ef, SFo, SFw, SFp]
177
262
 
178
263
 
179
264
  def run(cycle: dict):
180
- should_run, croppingDuration, country = _should_run(cycle)
181
- return _run(cycle, croppingDuration, country) if should_run else []
265
+ should_run, values = _should_run(cycle)
266
+ return _run(values) if should_run else []
@@ -796,26 +796,8 @@ def _preprocess_carbon_stocks(
796
796
  list[CarbonStock]
797
797
  A list of carbon stocks sorted by date.
798
798
  """
799
- sorted_measurements = sorted(
800
- flatten([split_node_by_dates(m) for m in carbon_stock_measurements]),
801
- key=lambda node: _gapfill_datestr(node["dates"][0], DatestrGapfillMode.END)
802
- )
803
-
804
- values = flatten(node["value"] for node in sorted_measurements)
805
-
806
- sds = flatten(
807
- node.get("sd", []) or [_calc_nominal_sd(v, _NOMINAL_ERROR) for v in node["value"]]
808
- for node in sorted_measurements
809
- )
810
-
811
- dates = flatten(
812
- [_gapfill_datestr(datestr, DatestrGapfillMode.END) for datestr in node["dates"]]
813
- for node in sorted_measurements
814
- )
815
-
816
- methods = flatten(
817
- [MeasurementMethodClassification(node.get("methodClassification")) for _ in node["value"]]
818
- for node in sorted_measurements
799
+ dates, values, sds, methods = _extract_node_data(
800
+ flatten([split_node_by_dates(m) for m in carbon_stock_measurements])
819
801
  )
820
802
 
821
803
  correlation_matrix = compute_time_series_correlation_matrix(
@@ -842,6 +824,35 @@ def _preprocess_carbon_stocks(
842
824
  ]
843
825
 
844
826
 
827
+ def _extract_node_data(nodes: list[dict]) -> list[dict]:
828
+
829
+ def group_node(result, node) -> dict[str, dict]:
830
+ date = _gapfill_datestr(node["dates"][0], DatestrGapfillMode.END)
831
+ result[date] = result.get(date, []) + [node]
832
+ return result
833
+
834
+ grouped_nodes = reduce(group_node, nodes, dict())
835
+
836
+ def get_values(date):
837
+ return flatten(node.get("value", []) for node in grouped_nodes[date])
838
+
839
+ def get_sds(date):
840
+ return flatten(
841
+ node.get("sd", []) or [_calc_nominal_sd(v, _NOMINAL_ERROR) for v in node.get("value", [])]
842
+ for node in grouped_nodes[date]
843
+ )
844
+
845
+ def get_methods(date):
846
+ return flatten(node.get("methodClassification", []) for node in grouped_nodes[date])
847
+
848
+ dates = sorted(grouped_nodes.keys())
849
+ values = [mean(get_values(date)) for date in dates]
850
+ sds = [mean(get_sds(date)) for date in dates]
851
+ methods = [min_measurement_method_classification(get_methods(date)) for date in dates]
852
+
853
+ return dates, values, sds, methods
854
+
855
+
845
856
  def _calc_nominal_sd(value: float, error: float) -> float:
846
857
  """
847
858
  Calculate a nominal SD for a carbon stock measurement. Can be used to gap fill SD when information not present in
@@ -1,5 +1,5 @@
1
1
  from hestia_earth.schema import EmissionMethodTier
2
- from hestia_earth.utils.tools import list_sum, safe_parse_float
2
+ from hestia_earth.utils.tools import list_sum, safe_parse_float, non_empty_list
3
3
  from hestia_earth.utils.model import find_term_match
4
4
 
5
5
  from hestia_earth.models.log import debugValues, logRequirements, logShouldRun
@@ -68,14 +68,21 @@ def _should_run(cycle: dict):
68
68
  uan_share = get_country_breakdown(MODEL, TERM_ID, country_id, LOOKUPS['inorganicFertiliser'][1])
69
69
  urea_unspecified_as_n = list_sum(find_term_match(inputs, UNSPECIFIED_TERM_ID).get('value', []))
70
70
 
71
- urea_values = [{'id': id, 'values': _get_urea_values(cycle, inputs, id)} for id in term_ids] + ([
72
- {'id': 'ureaKgN', 'values': [urea_unspecified_as_n * urea_share]},
73
- {'id': 'ureaAmmoniumNitrateKgN', 'values': [urea_unspecified_as_n * uan_share]}
74
- ] if all([
75
- urea_share,
76
- uan_share,
77
- urea_unspecified_as_n > 0
78
- ]) else [])
71
+ urea_values = [
72
+ {
73
+ 'id': id,
74
+ 'values': _get_urea_values(cycle, inputs, id)
75
+ } for id in term_ids
76
+ ] + non_empty_list([
77
+ {
78
+ 'id': 'ureaKgN',
79
+ 'values': [urea_unspecified_as_n * urea_share]
80
+ } if urea_share is not None else None,
81
+ {
82
+ 'id': 'ureaAmmoniumNitrateKgN',
83
+ 'values': [urea_unspecified_as_n * uan_share]
84
+ } if urea_share is not None else None
85
+ ] if urea_unspecified_as_n > 0 else [])
79
86
  has_urea_value = any([len(data.get('values')) > 0 for data in urea_values])
80
87
 
81
88
  logRequirements(cycle, model=MODEL, term=TERM_ID,
@@ -156,8 +156,6 @@ _Inventory = dict[int, _InventoryYear]
156
156
  {year (int): data (_InventoryYear)}
157
157
  """
158
158
 
159
- _EmissionInventory = dict[_EmissionTermId, npt.NDArray]
160
-
161
159
 
162
160
  _BIOMASS_CATEGORY_TO_FUEL_CATEGORY = {
163
161
  BiomassCategory.FOREST: {
@@ -518,9 +516,9 @@ def _sum_cycle_emissions(term_id: _EmissionTermId, cycle_id: str, inventory: _In
518
516
  return reduce(add_cycle_emissions, inventory.keys(), np.array(0))
519
517
 
520
518
 
521
- def _compile_run_data(
519
+ def _compile_inventory(
522
520
  cycle: dict, site: dict, land_cover_nodes: list[dict], eco_climate_zone: EcoClimateZone
523
- ) -> tuple[_EmissionInventory, _Inventory, dict]:
521
+ ):
524
522
  """
525
523
  Compile the run data for the model, collating data from `site.management` and related cycles. An annualised
526
524
  inventory of land cover change and natural vegetation burning events is constructed. Emissions from burning events
@@ -539,18 +537,12 @@ def _compile_run_data(
539
537
 
540
538
  Returns
541
539
  -------
542
- emission_inventory : _EmissionInventory
543
- A dictionary of emissions relevant to the cycle the model is run on, in the format:
544
- ```
545
- {
546
- emission_term_id (str): value (NDArray),
547
- ...
548
- }
549
- ```
540
+ should_run : bool
541
+ Whether the model should be run.
550
542
  inventory : _Inventory
551
- An inventory of model data
543
+ An inventory of model data.
552
544
  logs : dict
553
- Data from the compilation process that should be logged.
545
+ Data about the inventory compilation to be logged.
554
546
  """
555
547
  cycle_id = cycle.get("@id")
556
548
  related_cycles_ = related_cycles(site, cycles_mapping={cycle_id: cycle})
@@ -586,9 +578,15 @@ def _compile_run_data(
586
578
  Returns
587
579
  -------
588
580
  inventory : dict
589
- An inventory of model data, updated to include the new model year.
581
+ An inventory of model data, updated to include the input year.
590
582
  """
591
- land_cover_nodes = next((nodes for year_, nodes in land_cover_grouped.items() if year_ >= year), []) # Backfill
583
+ land_cover_nodes = land_cover_grouped.get(
584
+ next(
585
+ (k for k in sorted(land_cover_grouped) if k >= year), # backfill if possible
586
+ min(land_cover_grouped, key=lambda k: abs(k - year)) # else forward-fill
587
+ ),
588
+ []
589
+ )
592
590
 
593
591
  biomass_category_summary = summarise_land_cover_nodes(land_cover_nodes)
594
592
  prev_biomass_category_summary = inventory.get(year-1, {}).get("biomass_category_summary", {})
@@ -656,17 +654,17 @@ def _compile_run_data(
656
654
 
657
655
  inventory = reduce(build_inventory_year, range(min_year, max_year+1), dict())
658
656
 
659
- emission_inventory = {
660
- term_id: value for term_id in EMISSION_TERM_IDS
661
- if np.all((value := _sum_cycle_emissions(term_id, cycle_id, inventory)) > 0)
662
- }
657
+ n_land_cover_years = len(land_cover_grouped)
663
658
 
664
659
  logs = {
660
+ "n_land_cover_years": n_land_cover_years,
665
661
  "percent_burned": percent_burned,
666
662
  "seed": seed,
667
663
  }
668
664
 
669
- return emission_inventory, inventory, logs
665
+ should_run = bool(inventory and n_land_cover_years > 1)
666
+
667
+ return should_run, inventory, logs
670
668
 
671
669
 
672
670
  def _format_bool(value: Optional[bool]) -> str:
@@ -831,23 +829,16 @@ def _format_inventory(term_id: _EmissionTermId, cycle_id: str, inventory: dict)
831
829
  ) if inventory else "None"
832
830
 
833
831
 
834
- def _should_run_emission(
835
- term_id: _EmissionTermId, cycle: dict, emission_inventory: _EmissionInventory, inventory: dict, logs: dict
836
- ):
832
+ def _log_emission_data(should_run: bool, term_id: _EmissionTermId, cycle: dict, inventory: dict, logs: dict):
837
833
  """
838
- Determine, based on the compiled data, whether the model should run for a specifc emission term id. Format and log
839
- the model logs and inventory.
834
+ Format and log the model logs and inventory.
840
835
  """
841
- should_run = term_id in emission_inventory
842
-
843
836
  formatted_logs = _format_logs(logs)
844
837
  formatted_inventory = _format_inventory(term_id, cycle.get("@id"), inventory)
845
838
 
846
839
  logRequirements(cycle, model=MODEL, term=term_id, **formatted_logs, inventory=formatted_inventory)
847
840
  logShouldRun(cycle, MODEL, term_id, should_run)
848
841
 
849
- return should_run
850
-
851
842
 
852
843
  def _should_run(cycle: dict):
853
844
  """
@@ -861,8 +852,8 @@ def _should_run(cycle: dict):
861
852
 
862
853
  Returns
863
854
  -------
864
- tuple[bool, dict]
865
- should_run, emission_inventory
855
+ tuple[bool, _Inventory]
856
+ should_run, inventory
866
857
  """
867
858
  site = _get_site(cycle)
868
859
 
@@ -873,7 +864,7 @@ def _should_run(cycle: dict):
873
864
 
874
865
  has_valid_site_type = all([site_type, site_type not in _EXCLUDED_SITE_TYPES])
875
866
  has_valid_eco_climate_zone = all([eco_climate_zone, eco_climate_zone not in _EXCLUDED_ECO_CLIMATE_ZONES])
876
- has_land_cover_nodes = len(land_cover_nodes) > 0
867
+ has_land_cover_nodes = len(land_cover_nodes) > 1
877
868
 
878
869
  should_compile_inventory = all([
879
870
  has_valid_site_type,
@@ -881,9 +872,9 @@ def _should_run(cycle: dict):
881
872
  has_land_cover_nodes
882
873
  ])
883
874
 
884
- emission_inventory, inventory, compilation_logs = (
885
- _compile_run_data(cycle, site, land_cover_nodes, eco_climate_zone)
886
- if should_compile_inventory else ({}, {}, {})
875
+ should_run, inventory, compilation_logs = (
876
+ _compile_inventory(cycle, site, land_cover_nodes, eco_climate_zone)
877
+ if should_compile_inventory else (False, {}, {})
887
878
  )
888
879
 
889
880
  logs = {
@@ -897,21 +888,18 @@ def _should_run(cycle: dict):
897
888
  **compilation_logs
898
889
  }
899
890
 
900
- should_run = all([
901
- any([
902
- _should_run_emission(term_id, cycle, emission_inventory, inventory, logs) for term_id in EMISSION_TERM_IDS
903
- ])
904
- ])
891
+ for term_id in EMISSION_TERM_IDS:
892
+ _log_emission_data(should_run, term_id, cycle, inventory, logs)
905
893
 
906
- return should_run, emission_inventory
894
+ return should_run, inventory
907
895
 
908
896
 
909
- def _run_emission(term_id: _EmissionTermId, emissions: dict[_EmissionTermId, npt.NDArray]) -> list[dict]:
897
+ def _run_emission(term_id: _EmissionTermId, cycle_id: str, inventory: _Inventory) -> list[dict]:
910
898
  """
911
- Retrieve the pre-computed emissions and format them as a HESTIA
899
+ Retrieve the sum relevant emissions and format them as a HESTIA
912
900
  [Emission node](https://www.hestia.earth/schema/Emission).
913
901
  """
914
- emission = emissions[term_id]
902
+ emission = _sum_cycle_emissions(term_id, cycle_id, inventory)
915
903
  descriptive_stats = calc_descriptive_stats(emission, STATS_DEFINITION, decimals=3)
916
904
  return _emission(term_id, **descriptive_stats)
917
905
 
@@ -932,5 +920,5 @@ def run(cycle: dict):
932
920
  `ch4ToAirNaturalVegetationBurning` **OR** `coToAirNaturalVegetationBurning` **OR**
933
921
  `n2OToAirNaturalVegetationBurningDirect` **OR** `noxToAirNaturalVegetationBurning`.
934
922
  """
935
- should_run, emission_inventory = _should_run(cycle)
936
- return [_run_emission(term_id, emission_inventory) for term_id in emission_inventory] if should_run else []
923
+ should_run, inventory = _should_run(cycle)
924
+ return [_run_emission(term_id, cycle.get("@id"), inventory) for term_id in EMISSION_TERM_IDS] if should_run else []