hestia-earth-models 0.65.4__py3-none-any.whl → 0.65.6__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 (52) hide show
  1. hestia_earth/models/agribalyse2016/fuelElectricity.py +40 -24
  2. hestia_earth/models/aware/scarcityWeightedWaterUse.py +1 -1
  3. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsLandOccupation.py +1 -1
  4. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsLandTransformation.py +1 -1
  5. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsTotalLandUseEffects.py +1 -1
  6. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +9 -5
  7. hestia_earth/models/cycle/completeness/electricityFuel.py +4 -2
  8. hestia_earth/models/geospatialDatabase/precipitationAnnual.py +2 -2
  9. hestia_earth/models/geospatialDatabase/precipitationLongTermAnnualMean.py +2 -2
  10. hestia_earth/models/geospatialDatabase/precipitationMonthly.py +2 -2
  11. hestia_earth/models/geospatialDatabase/temperatureAnnual.py +2 -2
  12. hestia_earth/models/geospatialDatabase/temperatureLongTermAnnualMean.py +2 -2
  13. hestia_earth/models/geospatialDatabase/temperatureMonthly.py +2 -2
  14. hestia_earth/models/hestia/landCover.py +101 -68
  15. hestia_earth/models/hestia/landTransformation100YearAverageDuringCycle.py +49 -0
  16. hestia_earth/models/hestia/landTransformation20YearAverageDuringCycle.py +49 -0
  17. hestia_earth/models/hestia/resourceUse_utils.py +200 -0
  18. hestia_earth/models/hestia/seed_emissions.py +35 -21
  19. hestia_earth/models/hestia/utils.py +48 -0
  20. hestia_earth/models/impact_assessment/emissions.py +20 -5
  21. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +66 -28
  22. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +26 -142
  23. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +3 -3
  24. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +8 -5
  25. hestia_earth/models/linkedImpactAssessment/utils.py +3 -1
  26. hestia_earth/models/mocking/search-results.json +27 -4504
  27. hestia_earth/models/pooreNemecek2018/freshwaterWithdrawalsDuringCycle.py +4 -1
  28. hestia_earth/models/schererPfister2015/nErosionSoilFlux.py +23 -14
  29. hestia_earth/models/schererPfister2015/pErosionSoilFlux.py +23 -15
  30. hestia_earth/models/schererPfister2015/utils.py +3 -5
  31. hestia_earth/models/site/management.py +82 -22
  32. hestia_earth/models/utils/blank_node.py +28 -0
  33. hestia_earth/models/utils/crop.py +5 -1
  34. hestia_earth/models/utils/fuel.py +4 -1
  35. hestia_earth/models/utils/impact_assessment.py +7 -5
  36. hestia_earth/models/utils/pesticideAI.py +1 -0
  37. hestia_earth/models/version.py +1 -1
  38. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.6.dist-info}/METADATA +2 -2
  39. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.6.dist-info}/RECORD +52 -46
  40. tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +1 -1
  41. tests/models/hestia/test_landCover.py +2 -1
  42. tests/models/hestia/test_landTransformation100YearAverageDuringCycle.py +30 -0
  43. tests/models/hestia/test_landTransformation20YearAverageDuringCycle.py +31 -0
  44. tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChange.py +3 -1
  45. tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChange.py +3 -1
  46. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +3 -1
  47. tests/models/ipcc2019/test_organicCarbonPerHa.py +3 -2
  48. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +15 -11
  49. tests/models/utils/test_blank_node.py +22 -7
  50. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.6.dist-info}/LICENSE +0 -0
  51. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.6.dist-info}/WHEEL +0 -0
  52. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,200 @@
1
+ """
2
+ Resource Use
3
+
4
+ Provides common code for land transformation models.
5
+ """
6
+ from datetime import datetime
7
+ from dateutil.relativedelta import relativedelta
8
+ from hestia_earth.schema import TermTermType
9
+ from hestia_earth.utils.tools import to_precision
10
+
11
+ from hestia_earth.models.log import logRequirements, logShouldRun
12
+ from hestia_earth.models.utils.blank_node import _gapfill_datestr, DatestrGapfillMode, DatestrFormat, _str_dates_match
13
+ from hestia_earth.models.utils.impact_assessment import get_site
14
+ from hestia_earth.models.utils.indicator import _new_indicator
15
+ from .utils import (
16
+ LAND_USE_TERMS_FOR_TRANSFORMATION,
17
+ crop_ipcc_land_use_category,
18
+ )
19
+ from . import MODEL
20
+
21
+ _MAXIMUM_OFFSET_DAYS = 365 * 2
22
+ _OUTPUT_SIGNIFICANT_DIGITS = 3
23
+ _RESOURCE_USE_TERM_ID = 'landOccupationDuringCycle'
24
+
25
+
26
+ def _new_indicator_with_value(
27
+ term_id: str,
28
+ land_cover_id: str,
29
+ previous_land_cover_id: str,
30
+ value: float
31
+ ) -> dict:
32
+ indicator = _new_indicator(
33
+ term=term_id,
34
+ model=MODEL,
35
+ land_cover_id=land_cover_id,
36
+ previous_land_cover_id=previous_land_cover_id
37
+ )
38
+ indicator["value"] = to_precision(number=value, digits=_OUTPUT_SIGNIFICANT_DIGITS) if value != 0 else 0
39
+ return indicator
40
+
41
+
42
+ def _gap_filled_date_obj(date_str: str) -> datetime:
43
+ return datetime.strptime(
44
+ _gapfill_datestr(datestr=date_str, mode=DatestrGapfillMode.MIDDLE),
45
+ DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND.value
46
+ )
47
+
48
+
49
+ def _should_run_close_date_found(
50
+ ia_end_date_str: str,
51
+ management_nodes: list,
52
+ historic_date_offset: int
53
+ ) -> tuple[bool, str]:
54
+ historic_ia_date_obj = (
55
+ _gap_filled_date_obj(ia_end_date_str) - relativedelta(years=historic_date_offset)
56
+ if ia_end_date_str else None
57
+ )
58
+ # Calculate all distances in days which are less than MAXIMUM_OFFSET_DAYS from historic date
59
+ # Assumption: if there are two dates are equidistant from the target chose the second.
60
+ filtered_dates = {
61
+ abs((_gap_filled_date_obj(node.get("endDate")) - historic_ia_date_obj).days): node.get("endDate")
62
+ for node in management_nodes
63
+ if node.get("term", {}).get("termType", "") == TermTermType.LANDCOVER.value and
64
+ abs((_gap_filled_date_obj(node.get("endDate")) - historic_ia_date_obj).days) <= _MAXIMUM_OFFSET_DAYS
65
+ }
66
+ nearest_date = filtered_dates[min(filtered_dates.keys())] if filtered_dates else ""
67
+
68
+ return nearest_date != "", nearest_date
69
+
70
+
71
+ def should_run(
72
+ impact_assessment: dict,
73
+ site: dict,
74
+ term_id: str,
75
+ historic_date_offset: int
76
+ ) -> tuple[bool, dict, str]:
77
+ relevant_emission_resource_use = [
78
+ node for node in impact_assessment.get("emissionsResourceUse", [])
79
+ if node.get("term", {}).get("@id", "") == _RESOURCE_USE_TERM_ID and node.get("value", -1) >= 0
80
+ ]
81
+
82
+ filtered_management_nodes = [
83
+ node for node in site.get("management", [])
84
+ if node.get("value", -1) >= 0 and node.get("term", {}).get("termType", "") == TermTermType.LANDCOVER.value
85
+ ]
86
+ current_node_index = next(
87
+ (i for i, node in enumerate(filtered_management_nodes)
88
+ if _str_dates_match(node.get("endDate", ""), impact_assessment.get("endDate", ""))),
89
+ None
90
+ )
91
+ current_node = filtered_management_nodes.pop(current_node_index) if current_node_index is not None else None
92
+
93
+ close_date_found, closest_date_str = _should_run_close_date_found(
94
+ ia_end_date_str=impact_assessment.get("endDate", ""),
95
+ management_nodes=filtered_management_nodes,
96
+ historic_date_offset=historic_date_offset
97
+ )
98
+
99
+ logRequirements(
100
+ log_node=impact_assessment,
101
+ model=MODEL,
102
+ term_id=term_id,
103
+ site=site
104
+ )
105
+
106
+ should_run_result = all([
107
+ relevant_emission_resource_use != [],
108
+ current_node,
109
+ close_date_found
110
+ ])
111
+ logShouldRun(site, MODEL, term=term_id, should_run=should_run_result)
112
+
113
+ return should_run_result, current_node, closest_date_str
114
+
115
+
116
+ def _get_land_occupation_for_land_use_type(impact_assessment: dict, ipcc_land_use_category: str) -> float:
117
+ """
118
+ Returns the sum of all land occupation for the specified land_use_category.
119
+ """
120
+ return sum(
121
+ node.get("value", 0) for node in impact_assessment.get("emissionsResourceUse", [])
122
+ if node.get("term", {}).get("@id", "") == _RESOURCE_USE_TERM_ID
123
+ and crop_ipcc_land_use_category(node.get("landCover", {}).get("@id", "")) == ipcc_land_use_category
124
+ )
125
+
126
+
127
+ def _calculate_indicator_value(
128
+ impact_assessment: dict,
129
+ management_nodes: list,
130
+ ipcc_land_use_category: str,
131
+ previous_land_cover_id: str,
132
+ historic_date_offset: int
133
+ ) -> float:
134
+ """
135
+ Land transformation from [land type] previous management nodes
136
+ = (Land occupation, during Cycle * Historic Site Percentage Area [land type] / 100) / HISTORIC_DATE_OFFSET
137
+ """
138
+ land_occupation_for_cycle = _get_land_occupation_for_land_use_type(
139
+ impact_assessment=impact_assessment,
140
+ ipcc_land_use_category=ipcc_land_use_category
141
+ )
142
+ historical_land_use = sum(
143
+ node.get("value", 0) for node in management_nodes
144
+ if node.get("term", {}).get("@id", "") == previous_land_cover_id
145
+ )
146
+ return ((land_occupation_for_cycle * historical_land_use) / 100) / historic_date_offset
147
+
148
+
149
+ def _run_calculate_transformation(
150
+ term_id: str,
151
+ current_node: dict,
152
+ closest_date_str: str,
153
+ impact_assessment: dict,
154
+ site: dict,
155
+ historic_date_offset: int
156
+ ) -> list:
157
+ """
158
+ Calculate land transformation for all land use categories.
159
+ """
160
+ indicators = [
161
+ _new_indicator_with_value(
162
+ term_id=term_id,
163
+ land_cover_id=current_node.get("term", {}).get("@id"),
164
+ previous_land_cover_id=previous_land_cover_id,
165
+ value=_calculate_indicator_value(
166
+ impact_assessment=impact_assessment,
167
+ management_nodes=[
168
+ node for node in site.get("management", [])
169
+ if _str_dates_match(node.get("endDate", ""), closest_date_str)
170
+ ],
171
+ ipcc_land_use_category=crop_ipcc_land_use_category(current_node.get("term", {}).get("@id", "")),
172
+ previous_land_cover_id=previous_land_cover_id,
173
+ historic_date_offset=historic_date_offset
174
+ )
175
+ ) for previous_land_cover_id in [t[0] for t in LAND_USE_TERMS_FOR_TRANSFORMATION.values()]
176
+ ]
177
+
178
+ return indicators
179
+
180
+
181
+ def run_resource_use(
182
+ impact_assessment: dict,
183
+ historic_date_offset: int,
184
+ term_id: str
185
+ ) -> list:
186
+ site = get_site(impact_assessment)
187
+ _should_run, current_node, closest_date_str = should_run(
188
+ impact_assessment=impact_assessment,
189
+ site=site,
190
+ term_id=term_id,
191
+ historic_date_offset=historic_date_offset
192
+ )
193
+ return _run_calculate_transformation(
194
+ term_id=term_id,
195
+ current_node=current_node,
196
+ closest_date_str=closest_date_str,
197
+ site=site,
198
+ impact_assessment=impact_assessment,
199
+ historic_date_offset=historic_date_offset
200
+ ) if _should_run else []
@@ -18,7 +18,7 @@ from hestia_earth.utils.model import filter_list_term_type
18
18
  from hestia_earth.utils.tools import non_empty_list, flatten, list_sum, safe_parse_float
19
19
  from hestia_earth.utils.emission import cycle_emissions_in_system_boundary
20
20
 
21
- from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
21
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table, debugValues
22
22
  from hestia_earth.models.utils import _omit, group_by
23
23
  from hestia_earth.models.utils.emission import _new_emission
24
24
  from hestia_earth.models.utils.site import valid_site_type
@@ -54,7 +54,7 @@ REQUIREMENTS = {
54
54
  "siteType": ["cropland", "glass or high accessible cover"],
55
55
  "country": {"@type": "Term", "termType": "region"}
56
56
  },
57
- "emissions": [{"@type": "Emission"}]
57
+ "emissions": [{"@type": "Emission", "value": ""}]
58
58
  }
59
59
  }
60
60
  RETURNS = {
@@ -86,11 +86,24 @@ def _emission(term_id: str, value: float, input: dict):
86
86
  return emission
87
87
 
88
88
 
89
- def _run(cycle: dict, economicValueShare: float, total_yield: float, seed_input: dict, grouped_emissions: dict):
90
- term = seed_input.get('term', {})
89
+ def _run_emission(
90
+ cycle: dict, economicValueShare: float, total_yield: float, seed_input: dict, term_id: str, emission_value: float
91
+ ):
92
+ input_term = seed_input.get('term', {})
93
+ input_term_id = input_term.get('@id')
91
94
  seed_value = list_sum(seed_input.get('value'))
95
+ value = emission_value * economicValueShare / 100 / total_yield * seed_value
96
+ debugValues(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
97
+ value=value,
98
+ coefficient=1,
99
+ input=input_term_id)
100
+
101
+ return _emission(term_id, value, input_term)
102
+
103
+
104
+ def _run(cycle: dict, economicValueShare: float, total_yield: float, seed_input: dict, grouped_emissions: dict):
92
105
  return [
93
- _emission(term_id, emission_value * economicValueShare / 100 / total_yield * seed_value, term)
106
+ _run_emission(cycle, economicValueShare, total_yield, seed_input, term_id, emission_value)
94
107
  for term_id, emission_value in grouped_emissions.items()
95
108
  ]
96
109
 
@@ -102,12 +115,12 @@ def _filter_emissions(cycle: dict):
102
115
  {
103
116
  'id': i.get('term', {}).get('@id'),
104
117
  'group-id': get_lookup_value(i.get('term', {}), LOOKUPS['emission'], model=MODEL, model_key=MODEL_KEY),
105
- 'value': list_sum(i.get('value'), 0)
118
+ 'value': list_sum(i.get('value'))
106
119
  }
107
120
  for i in cycle.get('emissions', [])
108
121
  if all([
109
122
  i.get('term', {}).get('@id') in required_emission_term_ids,
110
- list_sum(i.get('value'), 0) > 0
123
+ len(i.get('value', [])) > 0
111
124
  ])
112
125
  ]
113
126
  emission_ids = set([v.get('id') for v in emissions])
@@ -120,7 +133,7 @@ def _filter_emissions(cycle: dict):
120
133
  'id': group_id,
121
134
  'emissions': list(filter(
122
135
  lambda id: id in required_emission_term_ids,
123
- list(lookup[lookup[column_name('inputProductionGroupId')] == group_id].termid)
136
+ set(list(lookup[lookup[column_name('inputProductionGroupId')] == group_id].termid))
124
137
  ))
125
138
  }
126
139
  for group_id in group_ids
@@ -129,8 +142,8 @@ def _filter_emissions(cycle: dict):
129
142
  {
130
143
  'id': group.get('id'),
131
144
  'total-emissions': len(group.get('emissions', [])),
132
- 'included-emissions': len([v in emission_ids for v in group.get('emissions', [])]),
133
- 'missing-emissions': '-'.join([v for v in group.get('emissions', []) if v not in emission_ids])
145
+ 'included-emissions': len(list(filter(lambda v: v in emission_ids, group.get('emissions', [])))),
146
+ 'missing-emissions': '-'.join(list(filter(lambda v: v not in emission_ids, group.get('emissions', []))))
134
147
  }
135
148
  for group in emissions_per_group
136
149
  ]
@@ -154,14 +167,14 @@ def _evs(product: dict):
154
167
  ) or product.get('economicValueShare')
155
168
 
156
169
 
157
- def _yield(country_id: str, end_year: int, product: dict):
170
+ def _faostat_yield(country_id: str, end_year: int, product: dict):
158
171
  grouping = get_crop_grouping_faostat_production(MODEL, product.get('term', {}))
159
172
  return safe_parse_float(extract_grouped_data_closest_date(get_table_value(
160
173
  download_lookup('region-crop-cropGroupingFaostatProduction-yield.csv'),
161
174
  'termid',
162
175
  country_id,
163
176
  column_name(grouping)
164
- ), end_year)) or list_sum(product.get('value'))
177
+ ), end_year))
165
178
 
166
179
 
167
180
  def _group_seed_inputs(inputs: list):
@@ -186,10 +199,11 @@ def _should_run(cycle: dict):
186
199
  {
187
200
  'product': product.get('term', {}).get('@id'),
188
201
  'seed-id': get_lookup_value(
189
- product.get('term', {}), 'correspondingSeedTermIds', model=MODEL, key=MODEL_KEY) or None,
202
+ product.get('term', {}), 'correspondingSeedTermIds', model=MODEL, model_key=MODEL_KEY) or None,
190
203
  'economicValueShare': _evs(product),
191
- 'yield': _yield(country_id, end_year, product),
192
- 'landCover-id': get_landCover_term_id(product.get('term', {}), model=MODEL, key=MODEL_KEY)
204
+ 'FAOSTAT-yield': _faostat_yield(country_id, end_year, product),
205
+ 'product-yield': list_sum(product.get('value')),
206
+ 'landCover-id': get_landCover_term_id(product.get('term', {}), model=MODEL, model_key=MODEL_KEY)
193
207
  }
194
208
  for product in crop_products
195
209
  ]
@@ -197,7 +211,7 @@ def _should_run(cycle: dict):
197
211
  value for value in crop_products if all([
198
212
  value.get('seed-id'),
199
213
  value.get('economicValueShare'),
200
- value.get('yield'),
214
+ value.get('FAOSTAT-yield') or value.get('product-yield'),
201
215
  value.get('landCover-id'),
202
216
  ])
203
217
  ]
@@ -209,7 +223,7 @@ def _should_run(cycle: dict):
209
223
  {
210
224
  'input': i,
211
225
  'is-corresponding-seed': i.get('term', {}).get('@id') in seed_term_ids,
212
- 'input-value': i.get('value'),
226
+ 'input-value': list_sum(i.get('value'), default=None),
213
227
  'has-linked-impact-assessment': bool(i.get('impactAssessment')),
214
228
  'is-fromCycle': i.get('fromCycle', False),
215
229
  'is-producedInCycle': i.get('producedInCycle', False),
@@ -221,7 +235,7 @@ def _should_run(cycle: dict):
221
235
  v.get('input') for v in seed_inputs
222
236
  if all([
223
237
  v.get('is-corresponding-seed', False),
224
- list_sum(v.get('input-value') or [-1]) > 0,
238
+ v.get('input-value') or -1 > 0,
225
239
  not v.get('has-linked-impact-assessment'),
226
240
  not v.get('is-fromCycle'),
227
241
  not v.get('is-producedInCycle'),
@@ -230,12 +244,12 @@ def _should_run(cycle: dict):
230
244
 
231
245
  crop_land_cover_ids = list(set([p.get('landCover-id') for p in valid_crop_products]))
232
246
  total_economicValueShare = list_sum([p.get('economicValueShare') for p in valid_crop_products])
233
- total_yield = list_sum([p.get('yield') for p in valid_crop_products])
247
+ total_yield = list_sum([p.get('FAOSTAT-yield') or p.get('product-yield') for p in valid_crop_products])
234
248
 
235
249
  emissions, emissions_per_group = _filter_emissions(cycle)
236
250
  # group emissions with the same group-id
237
251
  grouped_emissions = reduce(
238
- lambda p, c: p | {c.get('group-id'): p.get(c.get('group-id'), 0) + c.get('value', 0)},
252
+ lambda p, c: p | {c.get('group-id'): p.get(c.get('group-id'), 0) + (c.get('value') or 0)},
239
253
  emissions,
240
254
  {}
241
255
  )
@@ -266,7 +280,7 @@ def _should_run(cycle: dict):
266
280
  emissions_per_group=log_as_table(emissions_per_group),
267
281
  **_omit(seed_input, 'input'))
268
282
 
269
- logShouldRun(cycle, MODEL, term_id, should_run, key=MODEL_KEY)
283
+ logShouldRun(cycle, MODEL, term_id, should_run, methodTier=TIER, model_key=MODEL_KEY)
270
284
 
271
285
  return should_run, total_economicValueShare, total_yield, grouped_seed_inputs, grouped_emissions
272
286
 
@@ -0,0 +1,48 @@
1
+ from hestia_earth.schema import TermTermType
2
+
3
+ from hestia_earth.models.utils.term import get_lookup_value
4
+ from . import MODEL
5
+
6
+ IPCC_LAND_USE_CATEGORY_ANNUAL = "Annual crops"
7
+ IPCC_LAND_USE_CATEGORY_PERENNIAL = "Perennial crops"
8
+ TOTAL_CROPLAND = "Cropland"
9
+ ANNUAL_CROPLAND = "Arable land"
10
+ FOREST_LAND = "Forest land"
11
+ OTHER_LAND = "Other land"
12
+ PERMANENT_CROPLAND = "Permanent crops"
13
+ PERMANENT_PASTURE = "Permanent meadows and pastures"
14
+ TOTAL_AGRICULTURAL_CHANGE = "Total agricultural change"
15
+ ALL_LAND_USE_TERMS = [
16
+ FOREST_LAND,
17
+ TOTAL_CROPLAND,
18
+ ANNUAL_CROPLAND,
19
+ PERMANENT_CROPLAND,
20
+ PERMANENT_PASTURE,
21
+ OTHER_LAND
22
+ ]
23
+
24
+ # Mapping from Land use terms to Management node terms.
25
+ # land use term: (@id, name)
26
+ LAND_USE_TERMS_FOR_TRANSFORMATION = {
27
+ FOREST_LAND: ("forest", "Forest"),
28
+ ANNUAL_CROPLAND: ("annualCropland", "Annual cropland"),
29
+ PERMANENT_CROPLAND: ("permanentCropland", "Permanent cropland"),
30
+ PERMANENT_PASTURE: ("permanentPasture", "Permanent pasture"),
31
+ OTHER_LAND: ("otherLand", OTHER_LAND) # Not used yet
32
+ }
33
+
34
+
35
+ def crop_ipcc_land_use_category(
36
+ crop_term_id: str,
37
+ lookup_term_type: str = TermTermType.LANDCOVER.value
38
+ ) -> str:
39
+ """
40
+ Looks up the crop in the lookup.
41
+ Returns the IPCC_LAND_USE_CATEGORY.
42
+ """
43
+ return get_lookup_value(
44
+ lookup_term={"@id": crop_term_id, "type": "Term", "termType": lookup_term_type},
45
+ column='IPCC_LAND_USE_CATEGORY',
46
+ model=MODEL,
47
+ term={"@id": crop_term_id, "type": "Term", "termType": lookup_term_type}
48
+ )
@@ -4,9 +4,11 @@ Emissions
4
4
  Creates an [Indicator](https://hestia.earth/schema/Indicator) for every [Emission](https://hestia.earth/schema/Emission)
5
5
  contained within the [ImpactAssesment.cycle](https://hestia.earth/schema/ImpactAssessment#cycle).
6
6
  It does this by dividing the Emission amount by the Product amount, and applying an allocation between co-products.
7
+ Note: for any Emission in the system boundary that does not exist in the Cycle, it will log the model as failed,
8
+ so that we know the Emission is missing.
7
9
  """
8
-
9
10
  from hestia_earth.utils.tools import list_sum
11
+ from hestia_earth.utils.emission import cycle_emissions_in_system_boundary
10
12
 
11
13
  from hestia_earth.models.log import logRequirements, logShouldRun
12
14
  from hestia_earth.models.utils.impact_assessment import get_product, convert_value_from_cycle
@@ -38,10 +40,12 @@ RETURNS = {
38
40
  MODEL_KEY = 'emissions'
39
41
 
40
42
 
41
- def _indicator(product: dict):
43
+ def _indicator(impact_assessment: dict, product: dict):
42
44
  def run(emission: dict):
43
45
  term_id = emission.get('term', {}).get('@id')
44
- value = convert_value_from_cycle(product, list_sum(emission.get('value', [0])), model=MODEL, term_id=term_id)
46
+ value = convert_value_from_cycle(
47
+ impact_assessment, product, list_sum(emission.get('value', [0])), model=MODEL, term_id=term_id
48
+ )
45
49
 
46
50
  indicator = _new_indicator(emission.get('term', {}), emission.get('methodModel'))
47
51
  indicator['value'] = value
@@ -57,13 +61,23 @@ def _indicator(product: dict):
57
61
  return run
58
62
 
59
63
 
64
+ def _log_missing_emissions(impact_assessment: dict, emissions: list):
65
+ term_ids = cycle_emissions_in_system_boundary(cycle=impact_assessment.get('cycle', {}))
66
+ emission_ids = list(map(lambda e: e.get('term', {}).get('@id'), emissions))
67
+ missing_term_ids = [term_id for term_id in term_ids if term_id not in emission_ids]
68
+ for term_id in missing_term_ids:
69
+ logRequirements(impact_assessment, model=MODEL, term=term_id,
70
+ present_in_cycle=False)
71
+ logShouldRun(impact_assessment, MODEL, term_id, should_run=False)
72
+
73
+
60
74
  def _should_run_emission(impact_assessment: dict):
61
75
  product = get_product(impact_assessment)
62
76
 
63
77
  def exec(emission: dict):
64
78
  term_id = emission.get('term', {}).get('@id')
65
79
  has_value = convert_value_from_cycle(
66
- product, list_sum(emission.get('value', [0])), model=MODEL, term_id=term_id
80
+ impact_assessment, product, list_sum(emission.get('value', [0])), model=MODEL, term_id=term_id
67
81
  ) is not None
68
82
  not_deleted = emission.get('deleted', False) is not True
69
83
 
@@ -91,4 +105,5 @@ def run(impact_assessment: dict):
91
105
  should_run, product = _should_run(impact_assessment)
92
106
  emissions = impact_assessment.get('cycle', {}).get(MODEL_KEY, []) if should_run else []
93
107
  emissions = list(filter(_should_run_emission(impact_assessment), emissions))
94
- return list(map(_indicator(product), emissions))
108
+ _log_missing_emissions(impact_assessment, emissions)
109
+ return list(map(_indicator(impact_assessment, product), emissions))
@@ -47,7 +47,6 @@ _NOMINAL_ERROR = 75
47
47
  Carbon stock measurements without an associated `sd` should be assigned a nominal error of 75% (2*sd as a percentage of
48
48
  the mean).
49
49
  """
50
- _DEFAULT_YEARS_SINCE_LUC_EVENT = 999
51
50
  _TRANSITION_PERIOD_YEARS = 20
52
51
  _TRANSITION_PERIOD_DAYS = 20 * YEAR # 20 years in days
53
52
  _VALID_DATE_FORMATS = {
@@ -96,6 +95,7 @@ class _InventoryKey(Enum):
96
95
  LAND_USE_SUMMARY = "land-use-summary"
97
96
  LAND_USE_CHANGE_EVENT = "luc-event"
98
97
  YEARS_SINCE_LUC_EVENT = "years-since-luc-event"
98
+ YEARS_SINCE_INVENTORY_START = "years-since-inventory-start"
99
99
 
100
100
 
101
101
  CarbonStock = NamedTuple("CarbonStock", [
@@ -611,7 +611,6 @@ def _create_compile_inventory_function(
611
611
  )
612
612
 
613
613
  inventory = _squash_inventory(
614
- cycle_id,
615
614
  cycle_inventory,
616
615
  carbon_stock_inventory,
617
616
  land_use_inventory,
@@ -1008,14 +1007,20 @@ def _compile_land_use_inventory(
1008
1007
  is_luc_event = detect_land_use_change_func(land_use_summary, prev_land_use_summary)
1009
1008
 
1010
1009
  time_delta = current_year - prev_year
1011
- prev_years_since_luc_event = result.get(prev_year, {}).get(_InventoryKey.YEARS_SINCE_LUC_EVENT, 0)
1010
+ prev_years_since_luc_event = (
1011
+ result.get(prev_year, {}).get(_InventoryKey.YEARS_SINCE_LUC_EVENT, _TRANSITION_PERIOD_YEARS)
1012
+ )
1013
+ prev_years_since_inventory_start = result.get(prev_year, {}).get(_InventoryKey.YEARS_SINCE_INVENTORY_START, 0)
1014
+
1012
1015
  years_since_luc_event = time_delta if is_luc_event else prev_years_since_luc_event + time_delta
1016
+ years_since_inventory_start = prev_years_since_inventory_start + time_delta
1013
1017
 
1014
1018
  update_dict = {
1015
1019
  current_year: {
1016
1020
  _InventoryKey.LAND_USE_SUMMARY: land_use_summary,
1017
1021
  _InventoryKey.LAND_USE_CHANGE_EVENT: is_luc_event,
1018
1022
  _InventoryKey.YEARS_SINCE_LUC_EVENT: years_since_luc_event,
1023
+ _InventoryKey.YEARS_SINCE_INVENTORY_START: years_since_inventory_start
1019
1024
  }
1020
1025
  }
1021
1026
  return result | update_dict
@@ -1030,9 +1035,7 @@ def _compile_land_use_inventory(
1030
1035
  start_year: {
1031
1036
  _InventoryKey.LAND_USE_SUMMARY: summarise_land_use_func(
1032
1037
  land_cover_nodes_by_year.get(start_year, [])
1033
- ),
1034
- _InventoryKey.LAND_USE_CHANGE_EVENT: False,
1035
- _InventoryKey.YEARS_SINCE_LUC_EVENT: _DEFAULT_YEARS_SINCE_LUC_EVENT
1038
+ )
1036
1039
  }
1037
1040
  }
1038
1041
  ) if should_run else {}
@@ -1062,7 +1065,6 @@ def _sorted_merge(*sources: Union[dict, list[dict]]) -> dict:
1062
1065
 
1063
1066
 
1064
1067
  def _squash_inventory(
1065
- cycle_id: str,
1066
1068
  cycle_inventory: dict,
1067
1069
  carbon_stock_inventory: dict,
1068
1070
  land_use_inventory: dict,
@@ -1075,8 +1077,6 @@ def _squash_inventory(
1075
1077
 
1076
1078
  Parameters
1077
1079
  ----------
1078
- cycle_id : str
1079
- The unique identifier of the cycle being processed.
1080
1080
 
1081
1081
  cycle_inventory : dict
1082
1082
  A dictionary representing the share of emissions for each cycle, grouped by year.
@@ -1162,12 +1162,7 @@ def _squash_inventory(
1162
1162
  )))
1163
1163
 
1164
1164
  def should_run_group(method: MeasurementMethodClassification, year: int) -> bool:
1165
- carbon_stock_inventory_group = carbon_stock_inventory.get(method, {}).get(year, {})
1166
- share_of_emissions_group = cycle_inventory.get(year, {})
1167
-
1168
- has_emission = _InventoryKey.CO2_EMISSION in carbon_stock_inventory_group.keys()
1169
- is_relevant_for_cycle = cycle_id in share_of_emissions_group.get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
1170
- return all([has_emission, is_relevant_for_cycle])
1165
+ return _InventoryKey.CO2_EMISSION in carbon_stock_inventory.get(method, {}).get(year, {}).keys()
1171
1166
 
1172
1167
  def squash(result: dict, year: int) -> dict:
1173
1168
  update_dict = next(
@@ -1175,7 +1170,10 @@ def _squash_inventory(
1175
1170
  {
1176
1171
  year: {
1177
1172
  **_get_land_use_change_data(year, land_use_inventory),
1178
- **reduce(merge, [carbon_stock_inventory[method][year], cycle_inventory[year]], dict())
1173
+ **reduce(merge, [
1174
+ carbon_stock_inventory.get(method, {}).get(year, {}),
1175
+ cycle_inventory.get(year, {})
1176
+ ], dict())
1179
1177
  }
1180
1178
  } for method in measurement_method_ranking if should_run_group(method, year)
1181
1179
  ),
@@ -1205,15 +1203,22 @@ def _get_land_use_change_data(
1205
1203
  )
1206
1204
  )
1207
1205
 
1208
- delta_time = closest_inventory_year - year if closest_inventory_year else 0
1209
- inventory_data = land_use_inventory.get(closest_inventory_year, {})
1206
+ time_delta = closest_inventory_year - year if closest_inventory_year else 0
1207
+ prev_years_since_luc_event = (
1208
+ land_use_inventory.get(closest_inventory_year, {}).get(_InventoryKey.YEARS_SINCE_LUC_EVENT)
1209
+ )
1210
+ prev_years_since_inventory_start = (
1211
+ land_use_inventory.get(closest_inventory_year, {}).get(_InventoryKey.YEARS_SINCE_INVENTORY_START)
1212
+ )
1210
1213
 
1211
- years_since_luc_event = (
1212
- inventory_data.get(_InventoryKey.YEARS_SINCE_LUC_EVENT, _DEFAULT_YEARS_SINCE_LUC_EVENT) - delta_time
1214
+ years_since_luc_event = prev_years_since_luc_event - time_delta if prev_years_since_luc_event else None
1215
+ years_since_inventory_start = (
1216
+ prev_years_since_inventory_start - time_delta if prev_years_since_inventory_start else None
1213
1217
  )
1214
1218
 
1215
1219
  return {
1216
- _InventoryKey.YEARS_SINCE_LUC_EVENT: years_since_luc_event
1220
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: years_since_luc_event,
1221
+ _InventoryKey.YEARS_SINCE_INVENTORY_START: years_since_inventory_start
1217
1222
  }
1218
1223
 
1219
1224
 
@@ -1408,25 +1413,54 @@ def create_run_function(
1408
1413
  """
1409
1414
  data = inventory[year]
1410
1415
  years_since_luc_event = data[_InventoryKey.YEARS_SINCE_LUC_EVENT]
1411
- emission_term_id = (
1412
- land_use_change_emission_term_id if years_since_luc_event <= _TRANSITION_PERIOD_YEARS
1413
- else management_change_emission_term_id
1414
- )
1416
+ years_since_inventory_start = data[_InventoryKey.YEARS_SINCE_INVENTORY_START]
1417
+
1418
+ is_luc_emission = bool(years_since_luc_event) and years_since_luc_event <= _TRANSITION_PERIOD_YEARS
1419
+ is_data_complete = bool(years_since_inventory_start) and years_since_inventory_start > _TRANSITION_PERIOD_YEARS
1420
+
1421
+ if is_luc_emission:
1422
+ # If LUC emission allocate emissions to land use change AND add corresponding zero emission to management
1423
+ emission_term_id = land_use_change_emission_term_id
1424
+ zero_emission_term_id = management_change_emission_term_id
1425
+ elif is_data_complete:
1426
+ # If management emission && data complete allocate emissions to management change AND add corresponding
1427
+ # zero emission to management
1428
+ emission_term_id = management_change_emission_term_id
1429
+ zero_emission_term_id = land_use_change_emission_term_id
1430
+ else:
1431
+ # If management emission, but not data complete allocate emissions to management change only
1432
+ emission_term_id = management_change_emission_term_id
1433
+ zero_emission_term_id = None
1415
1434
 
1416
1435
  rescaled_emission = _rescale_carbon_stock_change_emission(
1417
1436
  data[_InventoryKey.CO2_EMISSION], data[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
1418
1437
  )
1419
1438
 
1439
+ zero_emission = CarbonStockChangeEmission(
1440
+ value=array(0),
1441
+ start_date=rescaled_emission.start_date,
1442
+ end_date=rescaled_emission.end_date,
1443
+ method=rescaled_emission.method
1444
+ ) if zero_emission_term_id else None
1445
+
1420
1446
  previous_emission = result.get(emission_term_id)
1447
+ previous_zero_emission = result.get(zero_emission_term_id)
1421
1448
 
1422
- update_dict = {
1449
+ emission_dict = {
1423
1450
  emission_term_id: (
1424
1451
  _add_carbon_stock_change_emissions(previous_emission, rescaled_emission) if previous_emission
1425
1452
  else rescaled_emission
1426
1453
  )
1427
1454
  }
1428
1455
 
1429
- return result | update_dict
1456
+ zero_emission_dict = {
1457
+ zero_emission_term_id: (
1458
+ _add_carbon_stock_change_emissions(previous_zero_emission, zero_emission) if previous_zero_emission
1459
+ else zero_emission
1460
+ )
1461
+ } if zero_emission_term_id else {}
1462
+
1463
+ return result | emission_dict | zero_emission_dict
1430
1464
 
1431
1465
  def run(cycle_id: str, cycle_start_date: str, cycle_end_date: str, inventory: dict) -> list[dict]:
1432
1466
  """
@@ -1447,9 +1481,13 @@ def create_run_function(
1447
1481
  list[dict]
1448
1482
  A list of [Emission](https://www.hestia.earth/schema/Emission) nodes containing model results.
1449
1483
  """
1484
+
1485
+ def should_run_year(year: int) -> bool:
1486
+ return cycle_id in inventory.get(year, {}).get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
1487
+
1450
1488
  assigned_emissions = reduce(
1451
1489
  lambda result, year: reduce_emissions(result, year, cycle_id, inventory),
1452
- inventory.keys(),
1490
+ (year for year in inventory.keys() if should_run_year(year)),
1453
1491
  {}
1454
1492
  )
1455
1493