hestia-earth-models 0.65.4__py3-none-any.whl → 0.65.5__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 (47) 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/cycle/completeness/electricityFuel.py +4 -2
  7. hestia_earth/models/geospatialDatabase/precipitationAnnual.py +2 -2
  8. hestia_earth/models/geospatialDatabase/precipitationLongTermAnnualMean.py +2 -2
  9. hestia_earth/models/geospatialDatabase/precipitationMonthly.py +2 -2
  10. hestia_earth/models/geospatialDatabase/temperatureAnnual.py +2 -2
  11. hestia_earth/models/geospatialDatabase/temperatureLongTermAnnualMean.py +2 -2
  12. hestia_earth/models/geospatialDatabase/temperatureMonthly.py +2 -2
  13. hestia_earth/models/hestia/landCover.py +31 -46
  14. hestia_earth/models/hestia/landTransformation100YearAverageDuringCycle.py +49 -0
  15. hestia_earth/models/hestia/landTransformation20YearAverageDuringCycle.py +49 -0
  16. hestia_earth/models/hestia/resourceUse_utils.py +200 -0
  17. hestia_earth/models/hestia/seed_emissions.py +35 -21
  18. hestia_earth/models/hestia/utils.py +48 -0
  19. hestia_earth/models/impact_assessment/emissions.py +20 -5
  20. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +66 -28
  21. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +26 -142
  22. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +3 -3
  23. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +8 -5
  24. hestia_earth/models/linkedImpactAssessment/utils.py +3 -1
  25. hestia_earth/models/mocking/search-results.json +779 -763
  26. hestia_earth/models/pooreNemecek2018/freshwaterWithdrawalsDuringCycle.py +4 -1
  27. hestia_earth/models/schererPfister2015/nErosionSoilFlux.py +23 -14
  28. hestia_earth/models/schererPfister2015/pErosionSoilFlux.py +23 -15
  29. hestia_earth/models/schererPfister2015/utils.py +3 -5
  30. hestia_earth/models/utils/blank_node.py +28 -0
  31. hestia_earth/models/utils/fuel.py +4 -1
  32. hestia_earth/models/utils/impact_assessment.py +7 -5
  33. hestia_earth/models/utils/pesticideAI.py +1 -0
  34. hestia_earth/models/version.py +1 -1
  35. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.5.dist-info}/METADATA +2 -2
  36. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.5.dist-info}/RECORD +47 -41
  37. tests/models/hestia/test_landTransformation100YearAverageDuringCycle.py +30 -0
  38. tests/models/hestia/test_landTransformation20YearAverageDuringCycle.py +31 -0
  39. tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChange.py +3 -1
  40. tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChange.py +3 -1
  41. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +3 -1
  42. tests/models/ipcc2019/test_organicCarbonPerHa.py +3 -2
  43. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +15 -11
  44. tests/models/utils/test_blank_node.py +22 -7
  45. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.5.dist-info}/LICENSE +0 -0
  46. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.5.dist-info}/WHEEL +0 -0
  47. {hestia_earth_models-0.65.4.dist-info → hestia_earth_models-0.65.5.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Fuel and Electricity
3
3
 
4
- This model calculates fuel and electricity data from the number of hours each machine is operated for using.
4
+ This model calculates fuel and electricity data from the number of hours each machine is used for.
5
5
  """
6
6
  from hestia_earth.schema import TermTermType
7
7
  from hestia_earth.utils.model import filter_list_term_type
@@ -68,43 +68,59 @@ def _run_operation(cycle: dict):
68
68
  return exec
69
69
 
70
70
 
71
- def _should_run_operation(cycle: dict):
72
- def exec(practice: dict):
73
- term = practice.get('term', {})
74
- term_id = term.get('@id')
75
- values = practice.get('value', [])
76
- value = list_sum(values) if all([not isinstance(v, str) for v in values]) else 0 # str allowed for Practice
77
- has_value = value > 0
71
+ def _operation_data(practice: dict):
72
+ term = practice.get('term', {})
73
+ values = practice.get('value', [])
74
+ value = list_sum(values) if all([not isinstance(v, str) for v in values]) else None # str allowed for Practice
78
75
 
79
- coeffs = get_lookup_value(term, LOOKUPS['operation'], model=MODEL, model_key=MODEL_KEY)
80
- values = non_empty_list(coeffs.split(';')) if coeffs else []
81
- inputs = [{'id': c.split(':')[0], 'value': float(c.split(':')[1])} for c in values]
82
- has_lookup_value = len(inputs) > 0
76
+ coeffs = get_lookup_value(term, LOOKUPS['operation'], model=MODEL, model_key=MODEL_KEY)
77
+ values = non_empty_list(coeffs.split(';')) if coeffs else []
78
+ inputs = [{'id': c.split(':')[0], 'value': float(c.split(':')[1])} for c in values]
83
79
 
84
- logRequirements(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
85
- has_value=has_value,
86
- has_fuelUse_lookup_value=has_lookup_value)
87
-
88
- should_run = all([has_value, has_lookup_value])
89
- logShouldRun(cycle, MODEL, term_id, should_run, model_key=MODEL_KEY)
90
- return [{'term': term, 'value': value, 'input': input} for input in inputs] if should_run else []
91
- return exec
80
+ return [{
81
+ 'term': term,
82
+ 'value': value,
83
+ 'input': input,
84
+ 'dates': ';'.join(practice.get('dates', []))
85
+ } for input in inputs]
92
86
 
93
87
 
94
88
  def _should_run(cycle: dict):
95
89
  is_incomplete = not cycle.get('completeness', {}).get('electricityFuel', False)
96
90
  operations = filter_list_term_type(cycle.get('practices', []), TermTermType.OPERATION)
97
- operations = flatten(map(_should_run_operation(cycle), operations))
98
- has_operations = len(operations) > 0
91
+
92
+ operations = flatten(map(_operation_data, operations))
93
+ term_ids = list(set(non_empty_list(map(lambda v: v.get('term', {}).get('@id'), operations))))
94
+
95
+ valid_operations = [v for v in operations if (v.get('value') or 0) > 0]
96
+ has_operations = len(valid_operations) > 0
97
+
98
+ # group operations by term to show in logs
99
+ grouped_operations = group_by(operations, ['term.@id'])
100
+
101
+ for term_id, operations in grouped_operations.items():
102
+ logs = [
103
+ {
104
+ 'value': operation.get('value'),
105
+ 'dates': operation.get('dates'),
106
+ 'input-id': operation.get('input', {}).get('@id'),
107
+ }
108
+ for operation in operations
109
+ ]
110
+ logRequirements(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
111
+ details=log_as_table(logs))
112
+
113
+ should_run = any([(v.get('value') or 0) > 0 for v in operations])
114
+ logShouldRun(cycle, MODEL, term_id, should_run, model_key=MODEL_KEY)
99
115
 
100
116
  logRequirements(cycle, model=MODEL, model_key=MODEL_KEY,
101
117
  is_term_type_electricityFuel_incomplete=is_incomplete,
102
118
  has_operations=has_operations,
103
- operations=';'.join(non_empty_list(map(lambda v: v.get('term', {}).get('@id'), operations))))
119
+ operations=';'.join(term_ids))
104
120
 
105
121
  should_run = all([is_incomplete, has_operations])
106
122
  logShouldRun(cycle, MODEL, None, should_run, model_key=MODEL_KEY)
107
- return should_run, operations
123
+ return should_run, valid_operations
108
124
 
109
125
 
110
126
  def run(cycle: dict):
@@ -89,7 +89,7 @@ def _run(impact_assessment: dict):
89
89
  _get_factor_from_basinId(site, aware_id) if aware_id else None
90
90
  ) or _get_factor_from_region(impact_assessment, site)
91
91
  inputs_value = convert_value_from_cycle(
92
- product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
92
+ impact_assessment, product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
93
93
  )
94
94
 
95
95
  logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
@@ -73,7 +73,7 @@ def _run(impact_assessment: dict):
73
73
  land_occupation_m2_kg = land_occupation_per_kg(MODEL, TERM_ID, cycle, site, product)
74
74
  factor = get_region_factor(TERM_ID, impact_assessment, LOOKUP_SUFFIX, 'medium_intensity')
75
75
  inputs_value = convert_value_from_cycle(
76
- product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
76
+ impact_assessment, product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
77
77
  )
78
78
  logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
79
79
  landOccupation=land_occupation_m2_kg,
@@ -68,7 +68,7 @@ def _run(impact_assessment: dict):
68
68
  landTransformation = _value(impact_assessment, _TRANSFORMATION_TERM_ID)
69
69
  region_factor = get_region_factor(TERM_ID, impact_assessment, _LOOKUP_SUFFIX, 'medium_intensity')
70
70
  inputs_value = convert_value_from_cycle(
71
- product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
71
+ impact_assessment, product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
72
72
  )
73
73
  logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
74
74
  landTransformation=landTransformation,
@@ -57,7 +57,7 @@ def run(impact_assessment: dict):
57
57
  cycle = impact_assessment.get('cycle', {})
58
58
  product = get_product(impact_assessment)
59
59
  inputs_value = convert_value_from_cycle(
60
- product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
60
+ impact_assessment, product, sum_input_impacts(cycle.get('inputs', []), TERM_ID), model=MODEL, term_id=TERM_ID
61
61
  )
62
62
  value = sum_values([landUseEffects, inputs_value])
63
63
  logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
@@ -40,7 +40,7 @@ def _lookup_value(practice: dict, lookup_name: str):
40
40
  def _practice_value(practice: dict):
41
41
  term = practice.get('term', {})
42
42
  fuel_use = _lookup_value(practice, LOOKUPS['operation'][0])
43
- return {'id': term.get('@id'), 'fuel_use': fuel_use}
43
+ return {'id': term.get('@id'), 'value': practice.get('value'), 'fuel_use': fuel_use}
44
44
 
45
45
 
46
46
  def run(cycle: dict):
@@ -55,6 +55,8 @@ def run(cycle: dict):
55
55
  term_type_operation_complete=operation_complete,
56
56
  values=log_as_table(practices_values))
57
57
 
58
- is_complete = all([operation_complete] + [p.get('fuel_use') for p in practices_values])
58
+ is_complete = all([operation_complete] + [
59
+ all([p.get('fuel_use'), p.get('value') is not None]) for p in practices_values
60
+ ])
59
61
 
60
62
  return is_complete
@@ -31,8 +31,8 @@ RETURNS = {
31
31
  }
32
32
  TERM_ID = 'precipitationAnnual'
33
33
  EE_PARAMS = {
34
- 'collection': 'ECMWF/ERA5/MONTHLY',
35
- 'band_name': 'total_precipitation',
34
+ 'collection': 'ECMWF/ERA5_LAND/MONTHLY_AGGR',
35
+ 'band_name': 'total_precipitation_sum',
36
36
  'ee_type': 'raster',
37
37
  'reducer': 'mean',
38
38
  'reducer_annual': 'sum'
@@ -27,8 +27,8 @@ TERM_ID = 'precipitationLongTermAnnualMean'
27
27
  START_DATE = '1979-01-01'
28
28
  END_DATE = '2020-12-31'
29
29
  EE_PARAMS = {
30
- 'collection': 'ECMWF/ERA5/MONTHLY',
31
- 'band_name': 'total_precipitation',
30
+ 'collection': 'ECMWF/ERA5_LAND/MONTHLY_AGGR',
31
+ 'band_name': 'total_precipitation_sum',
32
32
  'ee_type': 'raster',
33
33
  'reducer': 'mean',
34
34
  'reducer_annual': 'sum',
@@ -31,8 +31,8 @@ RETURNS = {
31
31
  }
32
32
  TERM_ID = 'precipitationMonthly'
33
33
  EE_PARAMS = {
34
- 'collection': 'ECMWF/ERA5/MONTHLY',
35
- 'band_name': 'total_precipitation',
34
+ 'collection': 'ECMWF/ERA5_LAND/MONTHLY_AGGR',
35
+ 'band_name': 'total_precipitation_sum',
36
36
  'ee_type': 'raster',
37
37
  'reducer': 'mean',
38
38
  'reducer_annual': 'sum'
@@ -31,8 +31,8 @@ RETURNS = {
31
31
  }
32
32
  TERM_ID = 'temperatureAnnual'
33
33
  EE_PARAMS = {
34
- 'collection': 'ECMWF/ERA5/MONTHLY',
35
- 'band_name': 'mean_2m_air_temperature',
34
+ 'collection': 'ECMWF/ERA5_LAND/MONTHLY_AGGR',
35
+ 'band_name': 'temperature_2m',
36
36
  'ee_type': 'raster',
37
37
  'reducer': 'mean',
38
38
  'reducer_annual': 'mean'
@@ -27,8 +27,8 @@ TERM_ID = 'temperatureLongTermAnnualMean'
27
27
  START_DATE = '1979-01-01'
28
28
  END_DATE = '2020-12-31'
29
29
  EE_PARAMS = {
30
- 'collection': 'ECMWF/ERA5/MONTHLY',
31
- 'band_name': 'mean_2m_air_temperature',
30
+ 'collection': 'ECMWF/ERA5_LAND/MONTHLY_AGGR',
31
+ 'band_name': 'temperature_2m',
32
32
  'ee_type': 'raster',
33
33
  'reducer': 'mean',
34
34
  'reducer_annual': 'mean',
@@ -31,8 +31,8 @@ RETURNS = {
31
31
  }
32
32
  TERM_ID = 'temperatureMonthly'
33
33
  EE_PARAMS = {
34
- 'collection': 'ECMWF/ERA5/MONTHLY',
35
- 'band_name': 'mean_2m_air_temperature',
34
+ 'collection': 'ECMWF/ERA5_LAND/MONTHLY_AGGR',
35
+ 'band_name': 'temperature_2m',
36
36
  'ee_type': 'raster',
37
37
  'reducer': 'mean',
38
38
  'reducer_annual': 'mean'
@@ -17,6 +17,20 @@ from hestia_earth.utils.tools import safe_parse_float, to_precision, non_empty_v
17
17
  from hestia_earth.models.log import logRequirements, log_as_table, logShouldRun
18
18
  from hestia_earth.models.utils.management import _new_management
19
19
  from hestia_earth.models.utils.term import get_lookup_value
20
+ from .utils import (
21
+ IPCC_LAND_USE_CATEGORY_ANNUAL,
22
+ IPCC_LAND_USE_CATEGORY_PERENNIAL,
23
+ LAND_USE_TERMS_FOR_TRANSFORMATION,
24
+ ANNUAL_CROPLAND,
25
+ PERMANENT_CROPLAND,
26
+ FOREST_LAND,
27
+ OTHER_LAND,
28
+ PERMANENT_PASTURE,
29
+ TOTAL_CROPLAND,
30
+ TOTAL_AGRICULTURAL_CHANGE,
31
+ ALL_LAND_USE_TERMS,
32
+ crop_ipcc_land_use_category,
33
+ )
20
34
  from . import MODEL
21
35
 
22
36
  REQUIREMENTS = {
@@ -69,30 +83,6 @@ LOOKUPS = {
69
83
  MODEL_KEY = 'landCover'
70
84
 
71
85
  LAND_AREA = LOOKUPS["region-faostatArea"][3]
72
- TOTAL_CROPLAND = "Cropland"
73
- ANNUAL_CROPLAND = "Arable land"
74
- FOREST_LAND = "Forest land"
75
- OTHER_LAND = "Other land"
76
- PERMANENT_CROPLAND = "Permanent crops"
77
- PERMANENT_PASTURE = "Permanent meadows and pastures"
78
- TOTAL_AGRICULTURAL_CHANGE = "Total agricultural change"
79
- ALL_LAND_USE_TERMS = [
80
- FOREST_LAND,
81
- TOTAL_CROPLAND,
82
- ANNUAL_CROPLAND,
83
- PERMANENT_CROPLAND,
84
- PERMANENT_PASTURE,
85
- OTHER_LAND
86
- ]
87
- # Mapping from Land use terms to Management node terms.
88
- # land use term: (@id, name)
89
- LAND_USE_TERMS_FOR_TRANSFORMATION = {
90
- FOREST_LAND: ("forest", "Forest"),
91
- ANNUAL_CROPLAND: ("annualCropland", "Annual cropland"),
92
- PERMANENT_CROPLAND: ("permanentCropland", "Permanent cropland"),
93
- PERMANENT_PASTURE: ("permanentPasture", "Permanent pasture"),
94
- OTHER_LAND: ("otherLand", OTHER_LAND) # Not used yet
95
- }
96
86
  SITE_TYPES = {
97
87
  SiteSiteType.CROPLAND.value,
98
88
  SiteSiteType.FOREST.value,
@@ -100,8 +90,6 @@ SITE_TYPES = {
100
90
  SiteSiteType.PERMANENT_PASTURE.value
101
91
  }
102
92
  DEFAULT_WINDOW_IN_YEARS = 20
103
- IPCC_LAND_USE_CATEGORY_ANNUAL = "Annual crops"
104
- IPCC_LAND_USE_CATEGORY_PERENNIAL = "Perennial crops"
105
93
  OUTPUT_SIGNIFICANT_DIGITS = 3
106
94
 
107
95
 
@@ -137,22 +125,6 @@ def _lookup_land_use_type(nodes: list) -> str:
137
125
  )
138
126
 
139
127
 
140
- def _crop_ipcc_land_use_category(
141
- crop_term_id: str,
142
- lookup_term_type: str = TermTermType.LANDCOVER.value
143
- ) -> str:
144
- """
145
- Looks up the crop in the lookup.
146
- Returns the IPCC_LAND_USE_CATEGORY.
147
- """
148
- return get_lookup_value(
149
- lookup_term={"@id": crop_term_id, "type": "Term", "termType": lookup_term_type},
150
- column=LOOKUPS.get("crop")[1],
151
- model=MODEL,
152
- term={"@id": crop_term_id, "type": "Term", "termType": lookup_term_type}
153
- )
154
-
155
-
156
128
  def get_changes(country_id: str, end_year: int) -> dict:
157
129
  """
158
130
  For each entry in ALL_LAND_USE_TERMS, creates a key: value in output dictionary, also TOTAL
@@ -399,7 +371,7 @@ def _get_complete_faostat_to_crop_mapping() -> dict:
399
371
  get_table_value(lookup, 'termid', crop_term_id, column_name("cropGroupingFaostatArea"))
400
372
  )
401
373
  if key:
402
- mappings[key].append(_crop_ipcc_land_use_category(crop_term_id=crop_term_id, lookup_term_type="crop"))
374
+ mappings[key].append(crop_ipcc_land_use_category(crop_term_id=crop_term_id, lookup_term_type="crop"))
403
375
  return {
404
376
  fao_name: max(set(crop_terms), key=crop_terms.count)
405
377
  for fao_name, crop_terms in mappings.items()
@@ -420,6 +392,18 @@ def _get_harvested_area(country_id: str, year: int, faostat_name: str) -> float:
420
392
  )
421
393
 
422
394
 
395
+ def _get_term_id_for_crop(nodes: set, land_type: str) -> str:
396
+ """Use the original crop term id for permanent/perennial crops and the land use id for other types."""
397
+ result = next(
398
+ (node for node in nodes if crop_ipcc_land_use_category(node[0]) == IPCC_LAND_USE_CATEGORY_PERENNIAL), None
399
+ )
400
+ return (
401
+ # Take first perennial crop - not multi-cropping
402
+ result[0] if land_type == PERMANENT_CROPLAND and result else
403
+ LAND_USE_TERMS_FOR_TRANSFORMATION[land_type][0]
404
+ )
405
+
406
+
423
407
  def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from: dict, start_year: int) -> list:
424
408
  """Creates a list of new management nodes, excluding any dates matching existing ones."""
425
409
  existing_nodes_set = {
@@ -435,7 +419,8 @@ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from
435
419
  "percentage": 0 if ratio == -0.0 else to_precision(
436
420
  number=ratio * 100,
437
421
  digits=OUTPUT_SIGNIFICANT_DIGITS
438
- )
422
+ ),
423
+ "term_id": _get_term_id_for_crop(existing_nodes_set, land_type=land_type)
439
424
  }
440
425
  for land_type, ratio in percentage_transformed_from.items()
441
426
  ]
@@ -443,7 +428,7 @@ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from
443
428
 
444
429
  return [
445
430
  _management(
446
- term_id=LAND_USE_TERMS_FOR_TRANSFORMATION[value.get("land_type")][0],
431
+ term_id=value.get("term_id"),
447
432
  value=value.get("percentage"),
448
433
  start_date=value.get("land_management_key")[1],
449
434
  end_date=value.get("land_management_key")[2]
@@ -698,7 +683,7 @@ def _should_run_historical_land_use_change_single_crop(
698
683
  sum_of_site_areas_is_100
699
684
  ]
700
685
  )
701
- logShouldRun(site, MODEL, term=term.get("@id"), should_run=should_run, key=MODEL_KEY)
686
+ logShouldRun(site, MODEL, term.get("@id"), should_run, model_key=MODEL_KEY)
702
687
 
703
688
  return should_run, site_area
704
689
 
@@ -0,0 +1,49 @@
1
+ """
2
+ Creates an [emissionsResourceUse](https://hestia.earth/schema/Emission) for every landCover land transformation.
3
+ contained within the [ImpactAssesment.cycle](https://hestia.earth/schema/ImpactAssessment#cycle), averaged over the last
4
+ 100 years.
5
+
6
+ It does this by multiplying the land occupation during the cycle by the
7
+ [Site](https://www-staging.hestia.earth/schema/Site) area 100 years ago and dividing by 100.
8
+
9
+ Land transformation from [land type] 100 years =
10
+ (Land occupation, during Cycle * Site Percentage Area 100 years ago [land type] / 100) / 100
11
+ """
12
+ from .resourceUse_utils import run_resource_use
13
+
14
+ REQUIREMENTS = {
15
+ "ImpactAssessment": {
16
+ "Site": {
17
+ "management": [{"@type": "Management", "value": ">=0", "term.termType": "landCover", "endDate": ""}]
18
+ },
19
+ "emissionsResourceUse": [
20
+ {
21
+ "@type": "Indicator",
22
+ "term.@id": "landOccupationDuringCycle",
23
+ "landCover": {
24
+ "@type": "Term",
25
+ "termType": "landCover"
26
+ },
27
+ "value": ">=0"
28
+ }
29
+ ],
30
+ "endDate": ""
31
+ }
32
+ }
33
+ RETURNS = {
34
+ "Indicator": [{
35
+ "value": "",
36
+ "landCover": "",
37
+ "previousLandCover": ""
38
+ }]
39
+ }
40
+ TERM_ID = 'landTransformation100YearAverageDuringCycle'
41
+ _HISTORIC_DATE_OFFSET = 100
42
+
43
+
44
+ def run(impact_assessment: dict):
45
+ return run_resource_use(
46
+ impact_assessment=impact_assessment,
47
+ historic_date_offset=_HISTORIC_DATE_OFFSET,
48
+ term_id=TERM_ID
49
+ )
@@ -0,0 +1,49 @@
1
+ """
2
+ Creates an [emissionsResourceUse](https://hestia.earth/schema/Emission) for every landCover land transformation.
3
+ contained within the [ImpactAssesment.cycle](https://hestia.earth/schema/ImpactAssessment#cycle), averaged over the last
4
+ 20 years.
5
+
6
+ It does this by multiplying the land occupation during the cycle by the
7
+ [Site](https://www-staging.hestia.earth/schema/Site) area 20 years ago and dividing by 20.
8
+
9
+ Land transformation from [land type] 20 years =
10
+ (Land occupation, during Cycle * Site Percentage Area 20 years ago [land type] / 100) / 20
11
+ """
12
+ from .resourceUse_utils import run_resource_use
13
+
14
+ REQUIREMENTS = {
15
+ "ImpactAssessment": {
16
+ "Site": {
17
+ "management": [{"@type": "Management", "value": ">=0", "term.termType": "landCover", "endDate": ""}]
18
+ },
19
+ "emissionsResourceUse": [
20
+ {
21
+ "@type": "Indicator",
22
+ "term.@id": "landOccupationDuringCycle",
23
+ "landCover": {
24
+ "@type": "Term",
25
+ "termType": "landCover"
26
+ },
27
+ "value": ">=0"
28
+ }
29
+ ],
30
+ "endDate": ""
31
+ }
32
+ }
33
+ RETURNS = {
34
+ "Indicator": [{
35
+ "value": "",
36
+ "landCover": "",
37
+ "previousLandCover": ""
38
+ }]
39
+ }
40
+ TERM_ID = 'landTransformation20YearAverageDuringCycle'
41
+ _HISTORIC_DATE_OFFSET = 20
42
+
43
+
44
+ def run(impact_assessment: dict):
45
+ return run_resource_use(
46
+ impact_assessment=impact_assessment,
47
+ historic_date_offset=_HISTORIC_DATE_OFFSET,
48
+ term_id=TERM_ID
49
+ )
@@ -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 []