hestia-earth-models 0.64.13__py3-none-any.whl → 0.65.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 (118) hide show
  1. hestia_earth/models/agribalyse2016/fuelElectricity.py +1 -1
  2. hestia_earth/models/cache_sites.py +15 -24
  3. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsLandTransformation.py +6 -9
  4. hestia_earth/models/cycle/input/hestiaAggregatedData.py +46 -22
  5. hestia_earth/models/cycle/materialAndSubstrate.py +158 -0
  6. hestia_earth/models/cycle/pre_checks/cache_sources.py +3 -25
  7. hestia_earth/models/cycle/product/economicValueShare.py +2 -2
  8. hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandTransformation.py +11 -33
  9. hestia_earth/models/faostat2018/landTransformation100YearAverageDuringCycle.py +34 -0
  10. hestia_earth/models/faostat2018/landTransformation20YearAverageDuringCycle.py +34 -0
  11. hestia_earth/models/faostat2018/utils.py +47 -3
  12. hestia_earth/models/hestia/landCover.py +5 -5
  13. hestia_earth/models/hestia/seed_emissions.py +275 -0
  14. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +2 -2
  15. hestia_earth/models/ipcc2019/belowGroundBiomass.py +8 -2
  16. hestia_earth/models/ipcc2019/biomass_utils.py +11 -4
  17. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +21 -12
  18. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -2
  19. hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChange.py +2 -1
  20. hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChange.py +2 -1
  21. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +8 -7
  22. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChange.py +2 -1
  23. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +28 -34
  24. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +8 -12
  25. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +13 -30
  26. hestia_earth/models/linkedImpactAssessment/{landTransformationFromCropland20YearAverageInputsProduction.py → landTransformation100YearAverageInputsProduction.py} +5 -2
  27. hestia_earth/models/linkedImpactAssessment/{landTransformationFromCropland100YearAverageInputsProduction.py → landTransformation20YearAverageInputsProduction.py} +5 -2
  28. hestia_earth/models/linkedImpactAssessment/utils.py +69 -12
  29. hestia_earth/models/mocking/__init__.py +1 -1
  30. hestia_earth/models/mocking/search-results.json +1026 -1026
  31. hestia_earth/models/pooreNemecek2018/excretaKgN.py +45 -41
  32. hestia_earth/models/pooreNemecek2018/excretaKgVs.py +89 -63
  33. hestia_earth/models/pooreNemecek2018/saplingsDepreciatedAmountPerCycle.py +8 -8
  34. hestia_earth/models/pooreNemecek2018/utils.py +60 -19
  35. hestia_earth/models/preload_requests.py +24 -4
  36. hestia_earth/models/schererPfister2015/nErosionSoilFlux.py +4 -3
  37. hestia_earth/models/schererPfister2015/pErosionSoilFlux.py +4 -3
  38. hestia_earth/models/schererPfister2015/utils.py +12 -9
  39. hestia_earth/models/site/management.py +70 -55
  40. hestia_earth/models/site/pre_checks/cache_sources.py +2 -20
  41. hestia_earth/models/utils/__init__.py +12 -1
  42. hestia_earth/models/utils/aggregated.py +1 -1
  43. hestia_earth/models/utils/blank_node.py +20 -12
  44. hestia_earth/models/utils/cache_sources.py +15 -0
  45. hestia_earth/models/utils/constant.py +3 -0
  46. hestia_earth/models/utils/crop.py +5 -0
  47. hestia_earth/models/utils/indicator.py +3 -1
  48. hestia_earth/models/version.py +1 -1
  49. {hestia_earth_models-0.64.13.dist-info → hestia_earth_models-0.65.0.dist-info}/METADATA +2 -2
  50. {hestia_earth_models-0.64.13.dist-info → hestia_earth_models-0.65.0.dist-info}/RECORD +81 -108
  51. tests/models/cml2001Baseline/test_abioticResourceDepletionMineralsAndMetals.py +1 -1
  52. tests/models/cycle/input/test_hestiaAggregatedData.py +5 -2
  53. tests/models/cycle/test_materialsAndSubstrate.py +49 -0
  54. tests/models/environmentalFootprintV3/test_soilQualityIndexLandTransformation.py +39 -28
  55. tests/models/{hyde32/test_landTransformationFromForest20YearAverageDuringCycle.py → faostat2018/test_landTransformation100YearAverageDuringCycle.py} +5 -5
  56. tests/models/{hyde32/test_landTransformationFromForest100YearAverageDuringCycle.py → faostat2018/test_landTransformation20YearAverageDuringCycle.py} +5 -5
  57. tests/models/faostat2018/test_utils.py +28 -0
  58. tests/models/hestia/test_landCover.py +2 -1
  59. tests/models/hestia/test_seed_emissions.py +27 -0
  60. tests/models/ipcc2019/test_aboveGroundBiomass.py +40 -4
  61. tests/models/ipcc2019/test_belowGroundBiomass.py +40 -4
  62. tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChange.py +52 -15
  63. tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChange.py +50 -14
  64. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +53 -32
  65. tests/models/ipcc2019/test_organicCarbonPerHa.py +91 -108
  66. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +33 -50
  67. tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +0 -52
  68. tests/models/linkedImpactAssessment/test_freshwaterWithdrawalsInputsProduction.py +6 -4
  69. tests/models/linkedImpactAssessment/test_landOccupationInputsProduction.py +6 -4
  70. tests/models/linkedImpactAssessment/{test_landTransformationFromForest100YearAverageInputsProduction.py → test_landTransformation100YearAverageInputsProduction.py} +7 -5
  71. tests/models/linkedImpactAssessment/{test_landTransformationFromForest20YearAverageInputsProduction.py → test_landTransformation20YearAverageInputsProduction.py} +7 -5
  72. tests/models/pooreNemecek2018/test_excretaKgN.py +2 -2
  73. tests/models/pooreNemecek2018/test_excretaKgVs.py +1 -1
  74. tests/models/pooreNemecek2018/test_utils.py +26 -0
  75. tests/models/site/test_management.py +10 -27
  76. tests/models/test_cache_sites.py +40 -12
  77. tests/models/utils/test_blank_node.py +0 -8
  78. tests/models/utils/test_cache_sources.py +21 -0
  79. hestia_earth/models/blonkConsultants2016/landTransformationFromForest20YearAverageDuringCycle.py +0 -90
  80. hestia_earth/models/faostat2018/landTransformationFromCropland100YearAverage.py +0 -74
  81. hestia_earth/models/faostat2018/landTransformationFromCropland20YearAverage.py +0 -74
  82. hestia_earth/models/hyde32/__init__.py +0 -13
  83. hestia_earth/models/hyde32/landTransformationFromCropland100YearAverageDuringCycle.py +0 -60
  84. hestia_earth/models/hyde32/landTransformationFromCropland20YearAverageDuringCycle.py +0 -60
  85. hestia_earth/models/hyde32/landTransformationFromForest100YearAverageDuringCycle.py +0 -60
  86. hestia_earth/models/hyde32/landTransformationFromForest20YearAverageDuringCycle.py +0 -60
  87. hestia_earth/models/hyde32/landTransformationFromOtherNaturalVegetation100YearAverageDuringCycle.py +0 -61
  88. hestia_earth/models/hyde32/landTransformationFromOtherNaturalVegetation20YearAverageDuringCycle.py +0 -61
  89. hestia_earth/models/hyde32/landTransformationFromPermanentPasture100YearAverageDuringCycle.py +0 -61
  90. hestia_earth/models/hyde32/landTransformationFromPermanentPasture20YearAverageDuringCycle.py +0 -61
  91. hestia_earth/models/hyde32/utils.py +0 -72
  92. hestia_earth/models/linkedImpactAssessment/landTransformationFromForest100YearAverageInputsProduction.py +0 -36
  93. hestia_earth/models/linkedImpactAssessment/landTransformationFromForest20YearAverageInputsProduction.py +0 -36
  94. hestia_earth/models/linkedImpactAssessment/landTransformationFromOtherNaturalVegetation100YearAverageInputsProduction.py +0 -36
  95. hestia_earth/models/linkedImpactAssessment/landTransformationFromOtherNaturalVegetation20YearAverageInputsProduction.py +0 -36
  96. hestia_earth/models/linkedImpactAssessment/landTransformationFromPermanentPasture100YearAverageInputsProduction.py +0 -36
  97. hestia_earth/models/linkedImpactAssessment/landTransformationFromPermanentPasture20YearAverageInputsProduction.py +0 -36
  98. tests/models/blonkConsultants2016/test_landTransformationFromForest20YearAverageDuringCycle.py +0 -36
  99. tests/models/cycle/pre_checks/test_cache_sources.py +0 -25
  100. tests/models/faostat2018/test_landTransformationFromCropland100YearAverage.py +0 -40
  101. tests/models/faostat2018/test_landTransformationFromCropland20YearAverage.py +0 -40
  102. tests/models/hyde32/__init__.py +0 -0
  103. tests/models/hyde32/test_landTransformationFromCropland100YearAverageDuringCycle.py +0 -21
  104. tests/models/hyde32/test_landTransformationFromCropland20YearAverageDuringCycle.py +0 -21
  105. tests/models/hyde32/test_landTransformationFromOtherNaturalVegetation100YearAverageDuringCycle.py +0 -23
  106. tests/models/hyde32/test_landTransformationFromOtherNaturalVegetation20YearAverageDuringCycle.py +0 -21
  107. tests/models/hyde32/test_landTransformationFromPermanentPasture100YearAverageDuringCycle.py +0 -21
  108. tests/models/hyde32/test_landTransformationFromPermanentPasture20YearAverageDuringCycle.py +0 -21
  109. tests/models/linkedImpactAssessment/test_landTransformationFromCropland100YearAverageInputsProduction.py +0 -23
  110. tests/models/linkedImpactAssessment/test_landTransformationFromCropland20YearAverageInputsProduction.py +0 -23
  111. tests/models/linkedImpactAssessment/test_landTransformationFromOtherNaturalVegetation100YearAverageInputsProduction.py +0 -23
  112. tests/models/linkedImpactAssessment/test_landTransformationFromOtherNaturalVegetation20YearAverageInputsProduction.py +0 -23
  113. tests/models/linkedImpactAssessment/test_landTransformationFromPermanentPasture100YearAverageInputsProduction.py +0 -24
  114. tests/models/linkedImpactAssessment/test_landTransformationFromPermanentPasture20YearAverageInputsProduction.py +0 -24
  115. tests/models/site/pre_checks/test_cache_sources.py +0 -21
  116. {hestia_earth_models-0.64.13.dist-info → hestia_earth_models-0.65.0.dist-info}/LICENSE +0 -0
  117. {hestia_earth_models-0.64.13.dist-info → hestia_earth_models-0.65.0.dist-info}/WHEEL +0 -0
  118. {hestia_earth_models-0.64.13.dist-info → hestia_earth_models-0.65.0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,16 @@
1
+ from numpy import recarray
1
2
  from hestia_earth.schema import TermTermType
2
3
  from hestia_earth.utils.api import download_hestia
3
4
  from hestia_earth.utils.lookup import download_lookup, get_table_value, column_name, extract_grouped_data_closest_date
4
- from hestia_earth.utils.tools import safe_parse_float
5
- from numpy import recarray
5
+ from hestia_earth.utils.tools import safe_parse_float, flatten
6
6
 
7
- from hestia_earth.models.log import logger, debugMissingLookup
7
+ from hestia_earth.models.log import logger, debugMissingLookup, logRequirements, logShouldRun, debugValues
8
8
  from hestia_earth.models.utils.animalProduct import (
9
9
  FAO_LOOKUP_COLUMN, FAO_EQUIVALENT_LOOKUP_COLUMN, get_animalProduct_lookup_value
10
10
  )
11
11
  from hestia_earth.models.utils.product import convert_product_to_unit
12
+ from hestia_earth.models.utils.impact_assessment import get_country_id, impact_end_year
13
+ from hestia_earth.models.utils.indicator import _new_indicator
12
14
  from . import MODEL
13
15
 
14
16
  LOOKUP_PREFIX = f"{TermTermType.REGION.value}-{TermTermType.ANIMALPRODUCT.value}-{FAO_LOOKUP_COLUMN}"
@@ -140,3 +142,45 @@ def get_change_in_harvested_area_for_crop(country_id: str, crop_name: str, start
140
142
  return _split_delta(
141
143
  get_table_value(lookup, 'termid', country_id, column_name(crop_name)), start_year, end_year
142
144
  )
145
+
146
+
147
+ def should_run_landTransformationFromCropland(term_id: str, impact: dict):
148
+ indicators = [
149
+ i for i in impact.get('emissionsResourceUse', [])
150
+ if all([
151
+ i.get('term', {}).get('@id') == term_id,
152
+ i.get('previousLandCover', {}).get('@id') == 'cropland',
153
+ i.get('value', -1) > 0
154
+ ])
155
+ ]
156
+ has_cropland = bool(indicators)
157
+
158
+ should_run = all([has_cropland])
159
+ logRequirements(impact, model=MODEL, term=term_id,
160
+ has_cropland_indicators=has_cropland)
161
+ logShouldRun(impact, MODEL, term_id, should_run)
162
+
163
+ return should_run, indicators
164
+
165
+
166
+ def run_landTransformationFromCropland(term_id: str, impact: dict, indicators: list, years: int):
167
+ country_id = get_country_id(impact)
168
+ end_year = impact_end_year(impact)
169
+ total, permanent, temporary = get_cropland_ratio(country_id, end_year - years, end_year)
170
+
171
+ debugValues(impact, model=MODEL, term_id=term_id,
172
+ diff_temporary_area=temporary,
173
+ diff_permanent_area=permanent,
174
+ diff_total_area=total)
175
+
176
+ return flatten([
177
+ [
178
+ _new_indicator(term_id, MODEL, indicator.get('landCover', {}).get('@id'), 'annualCropland') | {
179
+ 'value': indicator.get('value') * temporary / total
180
+ },
181
+ _new_indicator(term_id, MODEL, indicator.get('landCover', {}).get('@id'), 'permanentCropland') | {
182
+ 'value': indicator.get('value') * permanent / total
183
+ }
184
+ ]
185
+ for indicator in indicators
186
+ ]) if total is not None else []
@@ -198,14 +198,14 @@ def _estimate_maximum_forest_change(
198
198
  pasture_change=pasture_change,
199
199
  total_cropland_change=total_cropland_change
200
200
  ) if not positive_change else (
201
- total_agricultural_change
201
+ -total_agricultural_change
202
202
  if -min(forest_change, 0) > total_agricultural_change else
203
203
  min(forest_change, 0)
204
204
  )
205
205
 
206
206
 
207
207
  def _negative_agricultural_land_change(forest_change, pasture_change, total_cropland_change):
208
- return pasture_change if 0 < pasture_change < -min(forest_change, 0) \
208
+ return -pasture_change if 0 < pasture_change < -min(forest_change, 0) \
209
209
  else min(forest_change, 0) if pasture_change > 0 \
210
210
  else -total_cropland_change if 0 < total_cropland_change < -min(forest_change, 0) \
211
211
  else min(forest_change, 0) if 0 < total_cropland_change \
@@ -432,7 +432,7 @@ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from
432
432
  LAND_USE_TERMS_FOR_TRANSFORMATION[land_type], f"{start_year}-01-01", f"{start_year}-12-31"
433
433
  ),
434
434
  "land_type": land_type,
435
- "percentage": 0.0 if ratio == -0.0 else to_precision(
435
+ "percentage": 0 if ratio == -0.0 else to_precision(
436
436
  number=ratio * 100,
437
437
  digits=OUTPUT_SIGNIFICANT_DIGITS
438
438
  )
@@ -462,7 +462,7 @@ def get_ratio_of_expanded_area(country_id: str, fao_name: str, end_year: int) ->
462
462
  faostat_name=fao_name
463
463
  )
464
464
  return 0.0 if any([expansion is None, end_value is None]) else max(
465
- 0.0, _safe_divide(numerator=expansion, denominator=(end_value - expansion))
465
+ 0.0, _safe_divide(numerator=expansion, denominator=end_value)
466
466
  )
467
467
 
468
468
 
@@ -619,7 +619,7 @@ def _should_run_historical_land_use_change_single_crop(
619
619
  other_land_loss_to_pasture=other_land_loss_to[PERMANENT_PASTURE]
620
620
  )
621
621
 
622
- # BA to BD
622
+ # C14-G14
623
623
  shares_of_expansion = _get_shares_of_expansion(
624
624
  land_use_type=land_use_type,
625
625
  percent_annual_cropland_was=percent_annual_cropland_was,
@@ -0,0 +1,275 @@
1
+ """
2
+ Seed Emissions
3
+
4
+ This model uses the `emissions` recalculated from the current `Cycle` to estimate the `emissions` associated with
5
+ producing the `Input` with `termType` = `seed`.
6
+
7
+ These are the steps:
8
+
9
+ 1. Recalculate all emissions for the Cycle of interest;
10
+ 2. Group Cycle emissions based on their `inputProductionGroupId` lookup value and sum them up;
11
+ 3. Divide the sum of each emission group by the average crop yield (from FAOSTAT) to get emissions per kg of product;
12
+ 4. Multiply by `seed.value` to get total background `emissions` from seed production per ha.
13
+ """
14
+ from functools import reduce
15
+ from hestia_earth.schema import TermTermType, EmissionMethodTier, SiteSiteType
16
+ from hestia_earth.utils.lookup import download_lookup, column_name, get_table_value, extract_grouped_data_closest_date
17
+ from hestia_earth.utils.model import filter_list_term_type
18
+ from hestia_earth.utils.tools import non_empty_list, flatten, list_sum, safe_parse_float
19
+ from hestia_earth.utils.emission import cycle_emissions_in_system_boundary
20
+
21
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
22
+ from hestia_earth.models.utils.emission import _new_emission
23
+ from hestia_earth.models.utils.site import valid_site_type
24
+ from hestia_earth.models.utils.cycle import cycle_end_year
25
+ from hestia_earth.models.utils.crop import get_crop_grouping_faostat_production, get_landCover_term_id
26
+ from hestia_earth.models.utils.completeness import _is_term_type_complete
27
+ from hestia_earth.models.utils.blank_node import get_lookup_value
28
+ from . import MODEL
29
+
30
+ REQUIREMENTS = {
31
+ "Cycle": {
32
+ "completeness.product": "True",
33
+ "endDate": "",
34
+ "inputs": [{
35
+ "@type": "Input",
36
+ "term.termType": "seed",
37
+ "value": "> 0",
38
+ "none": {
39
+ "impactAssessment": "",
40
+ "fromCycle": "True",
41
+ "producedInCycle": "True"
42
+ }
43
+ }],
44
+ "products": [{
45
+ "@type": "Product",
46
+ "term.termType": "crop",
47
+ "optional": {
48
+ "economicValueShare": "> 0"
49
+ }
50
+ }],
51
+ "site": {
52
+ "@type": "Site",
53
+ "siteType": ["cropland", "glass or high accessible cover"],
54
+ "country": {"@type": "Term", "termType": "region"}
55
+ },
56
+ "emissions": [{"@type": "Emission"}]
57
+ }
58
+ }
59
+ RETURNS = {
60
+ "Emission": [{
61
+ "value": "",
62
+ "inputs": "",
63
+ "methodTier": "background"
64
+ }]
65
+ }
66
+ LOOKUPS = {
67
+ "region-crop-cropGroupingFaostatProduction-yield": "value from cropGroupingFaostatProduction and country",
68
+ "crop": [
69
+ "correspondingSeedTermIds",
70
+ "cropGroupingFaostatProduction",
71
+ "global_economic_value_share",
72
+ "landCoverTermId"
73
+ ],
74
+ "emission": "inputProductionGroupId"
75
+ }
76
+ MODEL_KEY = 'seed_emissions'
77
+ TIER = EmissionMethodTier.BACKGROUND.value
78
+
79
+
80
+ def _emission(term_id: str, value: float, input: dict):
81
+ emission = _new_emission(term_id, MODEL)
82
+ emission['value'] = [round(value, 7)]
83
+ emission['inputs'] = [input]
84
+ emission['methodTier'] = TIER
85
+ return emission
86
+
87
+
88
+ def _run(cycle: dict, economicValueShare: float, total_yield: float, seed_input: dict, grouped_emissions: dict):
89
+ term = seed_input.get('term', {})
90
+ seed_value = list_sum(seed_input.get('value'))
91
+ return [
92
+ _emission(term_id, emission_value * economicValueShare / 100 / total_yield * seed_value, term)
93
+ for term_id, emission_value in grouped_emissions.items()
94
+ ]
95
+
96
+
97
+ def _filter_emissions(cycle: dict):
98
+ required_emission_term_ids = cycle_emissions_in_system_boundary(cycle)
99
+
100
+ emissions = [
101
+ {
102
+ 'id': i.get('term', {}).get('@id'),
103
+ 'group-id': get_lookup_value(i.get('term', {}), LOOKUPS['emission'], model=MODEL, model_key=MODEL_KEY),
104
+ 'value': list_sum(i.get('value'), 0)
105
+ }
106
+ for i in cycle.get('emissions', [])
107
+ if all([
108
+ i.get('term', {}).get('@id') in required_emission_term_ids,
109
+ list_sum(i.get('value'), 0) > 0
110
+ ])
111
+ ]
112
+ emission_ids = set([v.get('id') for v in emissions])
113
+ group_ids = set([v.get('group-id') for v in emissions if v.get('group-id')])
114
+
115
+ # for each group, get the list of all required emissions
116
+ lookup = download_lookup('emission.csv')
117
+ emissions_per_group = [
118
+ {
119
+ 'id': group_id,
120
+ 'emissions': list(filter(
121
+ lambda id: id in required_emission_term_ids,
122
+ list(lookup[lookup[column_name('inputProductionGroupId')] == group_id].termid)
123
+ ))
124
+ }
125
+ for group_id in group_ids
126
+ ]
127
+ emissions_per_group = [
128
+ {
129
+ 'id': group.get('id'),
130
+ 'total-emissions': len(group.get('emissions', [])),
131
+ 'included-emissions': len([v in emission_ids for v in group.get('emissions', [])]),
132
+ 'missing-emissions': '-'.join([v for v in group.get('emissions', []) if v not in emission_ids])
133
+ }
134
+ for group in emissions_per_group
135
+ ]
136
+ # only keep groups that have all emissions present in the Cycle
137
+ valid_groups = list(filter(
138
+ lambda group: group.get('total-emissions') == group.get('included-emissions'),
139
+ emissions_per_group
140
+ ))
141
+ valid_group_ids = set([v.get('id') for v in valid_groups])
142
+
143
+ # finally, only return emissions which groups are valid
144
+ return list(filter(
145
+ lambda emission: emission.get('group-id') in valid_group_ids,
146
+ emissions
147
+ )), emissions_per_group
148
+
149
+
150
+ def _evs(product: dict):
151
+ return safe_parse_float(
152
+ get_lookup_value(product.get('term', {}), 'global_economic_value_share', model=MODEL, model_key=MODEL_KEY)
153
+ ) or product.get('economicValueShare')
154
+
155
+
156
+ def _yield(country_id: str, end_year: int, product: dict):
157
+ grouping = get_crop_grouping_faostat_production(MODEL, product.get('term', {}))
158
+ return safe_parse_float(extract_grouped_data_closest_date(get_table_value(
159
+ download_lookup('region-crop-cropGroupingFaostatProduction-yield.csv'),
160
+ 'termid',
161
+ country_id,
162
+ column_name(grouping)
163
+ ), end_year)) or list_sum(product.get('value'))
164
+
165
+
166
+ def _group_seed_inputs(inputs: list):
167
+ def _group_by(group: dict, input: dict):
168
+ term_id = input.get('term', {}).get('@id')
169
+ group[term_id] = group.get(term_id, []) + [input]
170
+ return group
171
+
172
+ grouped_inputs = reduce(_group_by, inputs, {})
173
+ return [
174
+ inputs[0] | {'value': flatten([v.get('value') for v in inputs])}
175
+ for inputs in grouped_inputs.values()
176
+ ]
177
+
178
+
179
+ def _should_run(cycle: dict):
180
+ crop_products = filter_list_term_type(cycle.get('products', []), TermTermType.CROP)
181
+ site_type_valid = valid_site_type(cycle.get('site', {}), [
182
+ SiteSiteType.CROPLAND.value, SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value
183
+ ])
184
+ is_product_complete = _is_term_type_complete(cycle, 'product')
185
+ end_year = cycle_end_year(cycle)
186
+ country_id = cycle.get('site', {}).get('country', {}).get('@id')
187
+
188
+ # only keep the crop products that map to a seed
189
+ crop_products = [
190
+ {
191
+ 'product': product.get('term', {}).get('@id'),
192
+ 'seed-id': get_lookup_value(
193
+ product.get('term', {}), 'correspondingSeedTermIds', model=MODEL, key=MODEL_KEY),
194
+ 'economicValueShare': _evs(product),
195
+ 'yield': _yield(country_id, end_year, product),
196
+ 'landCover-id': get_landCover_term_id(product.get('term', {}), model=MODEL, key=MODEL_KEY)
197
+ }
198
+ for product in crop_products
199
+ ]
200
+ valid_crop_products = [
201
+ value for value in crop_products if all([
202
+ value.get('seed-id'),
203
+ value.get('economicValueShare'),
204
+ value.get('yield'),
205
+ value.get('landCover-id'),
206
+ ])
207
+ ]
208
+
209
+ # array of ; delimited values
210
+ seed_term_ids = list(set(flatten([v.get('seed-id').split(';') for v in valid_crop_products if v.get('seed-id')])))
211
+
212
+ seed_inputs = [
213
+ i
214
+ for i in cycle.get('inputs', [])
215
+ if all([
216
+ i.get('term', {}).get('termType') == TermTermType.SEED.value,
217
+ i.get('term', {}).get('@id') in seed_term_ids,
218
+ list_sum(i.get('value'), 0) > 0,
219
+ not i.get('impactAssessment'),
220
+ # ignore inputs which are flagged as Product of the Cycle
221
+ not i.get('fromCycle', False),
222
+ not i.get('producedInCycle', False)
223
+ ])
224
+ ]
225
+ # sum up seed inputs with the same id
226
+ seed_inputs = _group_seed_inputs(seed_inputs)
227
+
228
+ crop_land_cover_ids = list(set([p.get('landCover-id') for p in valid_crop_products]))
229
+ total_economicValueShare = list_sum([p.get('economicValueShare') for p in valid_crop_products])
230
+ total_yield = list_sum([p.get('yield') for p in valid_crop_products])
231
+
232
+ emissions, emissions_per_group = _filter_emissions(cycle)
233
+ # group emissions with the same group-id
234
+ grouped_emissions = reduce(
235
+ lambda p, c: p | {c.get('group-id'): p.get(c.get('group-id'), 0) + c.get('value', 0)},
236
+ emissions,
237
+ {}
238
+ )
239
+
240
+ should_run = all([
241
+ site_type_valid,
242
+ len(crop_land_cover_ids) <= 1,
243
+ is_product_complete,
244
+ total_economicValueShare,
245
+ total_yield,
246
+ bool(seed_inputs),
247
+ bool(emissions)
248
+ ])
249
+
250
+ for seed_input in seed_inputs:
251
+ term_id = seed_input.get('term', {}).get('@id')
252
+
253
+ logRequirements(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
254
+ site_type_valid=site_type_valid,
255
+ crop_products=log_as_table(crop_products),
256
+ crop_land_cover_ids=';'.join(crop_land_cover_ids),
257
+ is_term_type_product_complete=is_product_complete,
258
+ total_economicValueShare=total_economicValueShare,
259
+ total_yield=total_yield,
260
+ end_year=end_year,
261
+ country_id=country_id,
262
+ emissions=log_as_table(emissions),
263
+ emissions_per_group=log_as_table(emissions_per_group))
264
+
265
+ logShouldRun(cycle, MODEL, term_id, should_run, key=MODEL_KEY)
266
+
267
+ return should_run, total_economicValueShare, total_yield, seed_inputs, grouped_emissions
268
+
269
+
270
+ def run(cycle: dict):
271
+ should_run, economicValueShare, total_yield, seed_inputs, grouped_emissions = _should_run(cycle)
272
+ return flatten(non_empty_list([
273
+ _run(cycle, economicValueShare, total_yield, seed_input, grouped_emissions)
274
+ for seed_input in seed_inputs
275
+ ])) if should_run else []
@@ -22,7 +22,7 @@ from hestia_earth.models.utils.measurement import _new_measurement
22
22
 
23
23
  from . import MODEL
24
24
  from .biomass_utils import (
25
- BiomassCategory, get_valid_land_cover_terms, detect_land_cover_change, group_by_biomass_category, group_by_term_id,
25
+ BiomassCategory, get_valid_management_nodes, detect_land_cover_change, group_by_biomass_category, group_by_term_id,
26
26
  sample_biomass_equilibrium, summarise_land_cover_nodes
27
27
  )
28
28
 
@@ -152,7 +152,7 @@ def _should_run(site: dict) -> tuple[bool, dict, dict]:
152
152
  site_type = site.get("siteType")
153
153
  eco_climate_zone = get_eco_climate_zone_value(site, as_enum=True)
154
154
 
155
- land_cover = get_valid_land_cover_terms(site)
155
+ land_cover = get_valid_management_nodes(site)
156
156
 
157
157
  has_valid_site_type = site_type not in _EXCLUDED_SITE_TYPES
158
158
  has_valid_eco_climate_zone = all([
@@ -22,7 +22,7 @@ from hestia_earth.models.utils.measurement import _new_measurement
22
22
 
23
23
  from . import MODEL
24
24
  from .biomass_utils import (
25
- BiomassCategory, get_valid_land_cover_terms, detect_land_cover_change, group_by_biomass_category,
25
+ BiomassCategory, get_valid_management_nodes, detect_land_cover_change, group_by_biomass_category,
26
26
  sample_biomass_equilibrium, summarise_land_cover_nodes
27
27
  )
28
28
 
@@ -70,12 +70,16 @@ RETURNS = {
70
70
  "statsDefinition": "simulated",
71
71
  "observations": "",
72
72
  "dates": "",
73
+ "depthUpper": "0",
74
+ "depthLower": "30",
73
75
  "methodClassification": "tier 1 model"
74
76
  }]
75
77
  }
76
78
  TERM_ID = 'belowGroundBiomass'
77
79
 
78
80
  _ITERATIONS = 10000
81
+ _DEPTH_UPPER = 0
82
+ _DEPTH_LOWER = 30
79
83
  _METHOD_CLASSIFICATION = MeasurementMethodClassification.TIER_1_MODEL.value
80
84
  _STATS_DEFINITION = MeasurementStatsDefinition.SIMULATED.value
81
85
 
@@ -145,7 +149,7 @@ def _should_run(site: dict) -> tuple[bool, dict, dict]:
145
149
  site_type = site.get("siteType")
146
150
  eco_climate_zone = get_eco_climate_zone_value(site, as_enum=True)
147
151
 
148
- land_cover = get_valid_land_cover_terms(site)
152
+ land_cover = get_valid_management_nodes(site)
149
153
 
150
154
  has_valid_site_type = site_type not in _EXCLUDED_SITE_TYPES
151
155
  has_valid_eco_climate_zone = all([
@@ -522,4 +526,6 @@ def _measurement(
522
526
  measurement = _new_measurement(TERM_ID) | {
523
527
  key: value for key, value in update_dict.items() if value
524
528
  }
529
+ measurement["depthUpper"] = _DEPTH_UPPER
530
+ measurement["depthLower"] = _DEPTH_LOWER
525
531
  return measurement
@@ -10,9 +10,9 @@ from hestia_earth.utils.blank_node import get_node_value
10
10
  from hestia_earth.utils.model import filter_list_term_type
11
11
 
12
12
  from hestia_earth.models.utils.array_builders import repeat_single, truncated_normal_1d
13
- from hestia_earth.models.utils.blank_node import validate_start_date_end_date
13
+ from hestia_earth.models.utils.blank_node import node_term_match, validate_start_date_end_date
14
14
  from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_ecoClimateZone_lookup_grouped_value
15
- from hestia_earth.models.utils.term import get_lookup_value
15
+ from hestia_earth.models.utils.term import get_cover_crop_property_terms, get_lookup_value
16
16
 
17
17
 
18
18
  LOOKUPS = {
@@ -409,9 +409,16 @@ _KWARGS_TO_SAMPLE_FUNC = {
409
409
  }
410
410
 
411
411
 
412
- def get_valid_land_cover_terms(site: dict) -> list[dict]:
412
+ def get_valid_management_nodes(site: dict) -> list[dict]:
413
413
  """Retrieve valid `landCover` nodes from a site's management."""
414
+ COVER_CROP_TERM_IDS = get_cover_crop_property_terms()
414
415
  return [
415
416
  node for node in filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
416
- if validate_start_date_end_date(node)
417
+ if (
418
+ validate_start_date_end_date(node)
419
+ and not any(
420
+ prop.get("value", False) for prop in node.get("properties", [])
421
+ if node_term_match(prop, COVER_CROP_TERM_IDS)
422
+ )
423
+ )
417
424
  ]
@@ -3,7 +3,7 @@ from hestia_earth.utils.lookup import column_name, download_lookup, get_table_va
3
3
  from hestia_earth.utils.model import find_primary_product, find_term_match
4
4
  from hestia_earth.utils.tools import list_sum, safe_parse_float
5
5
 
6
- from hestia_earth.models.log import debugMissingLookup, debugValues, logRequirements, logShouldRun
6
+ from hestia_earth.models.log import debugMissingLookup, debugValues, logRequirements, logShouldRun, log_as_table
7
7
  from hestia_earth.models.utils.blank_node import get_total_value_converted_with_min_ratio
8
8
  from hestia_earth.models.utils.input import get_feed_inputs
9
9
  from hestia_earth.models.utils.emission import _new_emission
@@ -103,13 +103,18 @@ def _emission(value: float, sd: float = None, min: float = None, max: float = No
103
103
  return emission
104
104
 
105
105
 
106
- def _run(feed: float, enteric_factor: float = None, enteric_sd: float = None, default_values=(None, None, None)):
107
- value = (feed * ((enteric_factor or default_values[0]) / 100)) / METHANE_EC
108
- min = (feed * (default_values[1] / 100)) / METHANE_EC if all([enteric_factor is None, default_values[1]]) else None
109
- max = (feed * (default_values[2] / 100)) / METHANE_EC if all([enteric_factor is None, default_values[2]]) else None
110
- description = f"Average Ym factor of {default_values[0]}% used as data missing to differentiate Ym." if all([
106
+ def _run(feed: float, enteric_factor: float = None, enteric_sd: float = None, default_values: dict = {}):
107
+ default_value = default_values.get('value')
108
+ value = (feed * ((enteric_factor or default_value) / 100)) / METHANE_EC
109
+ min = (feed * (default_values.get('min') / 100)) / METHANE_EC if all([
110
+ enteric_factor is None, default_values.get('min')
111
+ ]) else None
112
+ max = (feed * (default_values.get('max') / 100)) / METHANE_EC if all([
113
+ enteric_factor is None, default_values.get('max')
114
+ ]) else None
115
+ description = f"Average Ym factor of {default_value}% used as data missing to differentiate Ym." if all([
111
116
  enteric_factor is None,
112
- default_values[0] is not None
117
+ default_value is not None
113
118
  ]) else None
114
119
  return [
115
120
  _emission(value, enteric_sd, min, max, description)
@@ -141,10 +146,10 @@ DE_MAPPING = {
141
146
  def _get_grouped_data_key(keys: list, DE: float, NDF: float, ionophore: bool, milk_yield: float):
142
147
  # test conditions one by one and return the key associated for the first one that passes
143
148
  return (
144
- next(
149
+ (next(
145
150
  (key for key in keys if key in DE_NDF_MAPPING and DE_NDF_MAPPING[key](DE, NDF)),
146
151
  None
147
- ) or next(
152
+ ) if all([DE is not None, NDF is not None]) else None) or next(
148
153
  (key for key in keys if key in DE_MAPPING and DE_MAPPING[key](DE, ionophore)),
149
154
  None
150
155
  ) if DE else None
@@ -204,7 +209,11 @@ def _get_default_values(lookup, term: dict):
204
209
  value = get_table_value(lookup, 'termid', term_id, column_name(LOOKUPS['liveAnimal'][3])) if term_id else None
205
210
  min = get_table_value(lookup, 'termid', term_id, column_name(LOOKUPS['liveAnimal'][4])) if term_id else None
206
211
  max = get_table_value(lookup, 'termid', term_id, column_name(LOOKUPS['liveAnimal'][5])) if term_id else None
207
- return safe_parse_float(value, None), safe_parse_float(min, None), safe_parse_float(max, None)
212
+ return {
213
+ 'value': safe_parse_float(value, None),
214
+ 'min': safe_parse_float(min, None),
215
+ 'max': safe_parse_float(max, None)
216
+ }
208
217
 
209
218
 
210
219
  def _should_run(cycle: dict):
@@ -250,7 +259,7 @@ def _should_run(cycle: dict):
250
259
  milk_yield=milk_yield,
251
260
  enteric_factor=enteric_factor,
252
261
  enteric_sd=enteric_sd,
253
- default_values=';'.join(map(str, default_values)))
262
+ default_values=log_as_table(default_values))
254
263
 
255
264
  logRequirements(cycle, model=MODEL, term=TERM_ID,
256
265
  term_type_animalFeed_complete=is_animalFeed_complete,
@@ -259,7 +268,7 @@ def _should_run(cycle: dict):
259
268
 
260
269
  should_run = all([
261
270
  is_animalFeed_complete, is_freshForage_complete, total_feed,
262
- enteric_factor or default_values[0]
271
+ enteric_factor or default_values.get('value')
263
272
  ])
264
273
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
265
274
  return should_run, total_feed, enteric_factor, enteric_sd, default_values
@@ -6,7 +6,7 @@ from hestia_earth.utils.tools import safe_parse_float, list_sum
6
6
 
7
7
  from hestia_earth.models.log import debugValues, logRequirements, debugMissingLookup, logShouldRun, log_as_table
8
8
  from hestia_earth.models.utils import _filter_list_term_unit
9
- from hestia_earth.models.utils.constant import Units
9
+ from hestia_earth.models.utils.constant import Units, DAYS_PER_MONTH
10
10
  from hestia_earth.models.utils.completeness import _is_term_type_complete
11
11
  from hestia_earth.models.utils.productivity import PRODUCTIVITY, get_productivity
12
12
  from hestia_earth.models.utils.emission import _new_emission
@@ -40,7 +40,6 @@ RETURNS = {
40
40
  }
41
41
  TERM_ID = 'ch4ToAirExcreta'
42
42
  TIER = EmissionMethodTier.TIER_2.value
43
- DAYS_PER_MONTH = 365.25/12
44
43
 
45
44
 
46
45
  class DURATION(Enum):
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match
5
5
  from hestia_earth.models.utils.emission import _new_emission
6
6
 
7
- from .biomass_utils import detect_land_cover_change, summarise_land_cover_nodes
7
+ from .biomass_utils import detect_land_cover_change, get_valid_management_nodes, summarise_land_cover_nodes
8
8
  from .co2ToAirCarbonStockChange_utils import create_run_function, create_should_run_function
9
9
  from . import MODEL
10
10
 
@@ -133,6 +133,7 @@ def run(cycle: dict) -> list[dict]:
133
133
  """
134
134
  should_run_exec = create_should_run_function(
135
135
  carbon_stock_term_id=_CARBON_STOCK_TERM_ID,
136
+ get_valid_management_nodes_func=get_valid_management_nodes,
136
137
  should_compile_inventory_func=_should_compile_inventory_func,
137
138
  summarise_land_use_func=summarise_land_cover_nodes,
138
139
  detect_land_use_change_func=detect_land_cover_change,
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match
5
5
  from hestia_earth.models.utils.emission import _new_emission
6
6
 
7
- from .biomass_utils import detect_land_cover_change, summarise_land_cover_nodes
7
+ from .biomass_utils import detect_land_cover_change, get_valid_management_nodes, summarise_land_cover_nodes
8
8
  from .co2ToAirCarbonStockChange_utils import create_run_function, create_should_run_function
9
9
  from . import MODEL
10
10
 
@@ -125,6 +125,7 @@ def run(cycle: dict) -> list[dict]:
125
125
  """
126
126
  should_run_exec = create_should_run_function(
127
127
  carbon_stock_term_id=_CARBON_STOCK_TERM_ID,
128
+ get_valid_management_nodes_func=get_valid_management_nodes,
128
129
  should_compile_inventory_func=_should_compile_inventory_func,
129
130
  summarise_land_use_func=summarise_land_cover_nodes,
130
131
  detect_land_use_change_func=detect_land_cover_change,
@@ -13,10 +13,9 @@ from pydash.objects import merge
13
13
  from typing import Any, Callable, NamedTuple, Optional, Union
14
14
 
15
15
  from hestia_earth.schema import (
16
- EmissionMethodTier, EmissionStatsDefinition, MeasurementMethodClassification, TermTermType
16
+ EmissionMethodTier, EmissionStatsDefinition, MeasurementMethodClassification
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
20
19
  from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
21
20
 
22
21
  from hestia_earth.models.log import log_as_table
@@ -24,7 +23,7 @@ from hestia_earth.models.utils import pairwise
24
23
  from hestia_earth.models.utils.array_builders import correlated_normal_2d, gen_seed
25
24
  from hestia_earth.models.utils.blank_node import (
26
25
  _gapfill_datestr, _get_datestr_format, DatestrGapfillMode, DatestrFormat, group_nodes_by_year, node_term_match,
27
- split_node_by_dates, validate_start_date_end_date
26
+ split_node_by_dates
28
27
  )
29
28
  from hestia_earth.models.utils.constant import Units, get_atomic_conversion
30
29
  from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
@@ -327,6 +326,7 @@ def _add_carbon_stock_change_emissions(
327
326
  def create_should_run_function(
328
327
  *,
329
328
  carbon_stock_term_id: str,
329
+ get_valid_management_nodes_func: Callable[[dict], list[dict]],
330
330
  should_compile_inventory_func: Callable[[dict, list[dict], list[dict]], tuple[bool, dict]],
331
331
  summarise_land_use_func: Callable[[list[dict]], Any],
332
332
  detect_land_use_change_func: Callable[[Any, Any], bool],
@@ -351,6 +351,10 @@ def create_should_run_function(
351
351
  `(site: dict, cycles: list[dict], carbon_stock_measurements: list[dict]) -> (should_run: bool, logs: dict)`, to
352
352
  determine whether there is enough site and cycles data available to compile the carbon stock change inventory.
353
353
 
354
+ get_valid_management_nodes_func : Callable[[dict], list[dict]]
355
+ A function, with the signature... `(site: dict) -> management_nodes: list[dict]` to extract valid management
356
+ nodes from the site for building the land use inventory.
357
+
354
358
  summarise_land_use_func: Callable[[list[dict]], Any]
355
359
  A function with the signature `(nodes: list[dict]) -> Any`, to reduce a list of `landCover`
356
360
  [Management](https://www.hestia.earth/schema/Management) nodes into a land use summary that can be compared
@@ -418,10 +422,7 @@ def create_should_run_function(
418
422
  ])
419
423
  ]
420
424
 
421
- land_cover_nodes = [
422
- node for node in filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
423
- if validate_start_date_end_date(node)
424
- ]
425
+ land_cover_nodes = get_valid_management_nodes_func(site)
425
426
 
426
427
  seed = gen_seed(site) # All cycles linked to the same site should be consistent
427
428
  rng = random.default_rng(seed)