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
@@ -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,
@@ -1,7 +1,12 @@
1
1
  """
2
- This model calculates the abiotic resource depletion, fossil fuels. Based on the models CML 2002 (Guinée et al., 2002) and van Oers et al. 2002 (method, v.4.8)
2
+ This model calculates the abiotic resource depletion, fossil fuels. Based on the models CML 2002 (Guinée et al., 2002)
3
+ and van Oers et al. 2002 (method, v.4.8)
3
4
 
4
- >The earth contains a finite amount of non-renewable resources, such as fossil fuels like coal, oil and gas. The basic idea behind this impact category is that extracting resources today will force future generations to extract less or different resources. For example, the depletion of V2.0 – 25th August 2023 28 fossil fuels may lead to the non-availability of fossil fuels for future generations. The amount of materials contributing to resource use, fossils, are converted into MJ.
5
+ > The earth contains a finite amount of non-renewable resources, such as fossil fuels like coal, oil and gas.
6
+ The basic idea behind this impact category is that extracting resources today will force future generations to extract
7
+ less or different resources. For example, the depletion of V2.0 – 25th August 2023 28 fossil fuels may lead to the
8
+ non-availability of fossil fuels for future generations. The amount of materials contributing to resource use, fossils,
9
+ are converted into MJ.
5
10
 
6
11
  Source : [Life Cycle Assessment & the EF methods - Comprehensive coverage of impacts](https://green-business.ec.europa.eu/environmental-footprint-methods/life-cycle-assessment-ef-methods_en)
7
12
 
@@ -89,9 +94,8 @@ def download_all_non_renewable_terms(lookup_file_name: str) -> list:
89
94
  lookup = download_lookup(lookup_file_name)
90
95
  results = lookup[
91
96
  lookup[column_name("abioticResourceDepletionFossilFuelsCml2001Baseline")] == True # noqa: E712
92
- ]["termid"]
93
- terms_ids: list[str] = [str(entry) for entry in results]
94
- return terms_ids
97
+ ]["termid"]
98
+ return list(map(str, results))
95
99
 
96
100
 
97
101
  def _valid_resource_indicator(resource: dict) -> bool:
@@ -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'
@@ -6,6 +6,8 @@ functionality of the Blonk model.
6
6
  """
7
7
  import math
8
8
  from collections import defaultdict
9
+ from datetime import datetime, timedelta
10
+
9
11
  from hestia_earth.schema import SiteSiteType, TermTermType
10
12
  from hestia_earth.utils.lookup import (
11
13
  download_lookup, get_table_value, column_name,
@@ -17,7 +19,23 @@ from hestia_earth.utils.tools import safe_parse_float, to_precision, non_empty_v
17
19
  from hestia_earth.models.log import logRequirements, log_as_table, logShouldRun
18
20
  from hestia_earth.models.utils.management import _new_management
19
21
  from hestia_earth.models.utils.term import get_lookup_value
22
+ from .utils import (
23
+ IPCC_LAND_USE_CATEGORY_ANNUAL,
24
+ IPCC_LAND_USE_CATEGORY_PERENNIAL,
25
+ LAND_USE_TERMS_FOR_TRANSFORMATION,
26
+ ANNUAL_CROPLAND,
27
+ PERMANENT_CROPLAND,
28
+ FOREST_LAND,
29
+ OTHER_LAND,
30
+ PERMANENT_PASTURE,
31
+ TOTAL_CROPLAND,
32
+ TOTAL_AGRICULTURAL_CHANGE,
33
+ ALL_LAND_USE_TERMS,
34
+ crop_ipcc_land_use_category,
35
+ )
20
36
  from . import MODEL
37
+ from ..utils.blank_node import _node_date, DatestrFormat
38
+ from ..utils.constant import DAYS_IN_YEAR
21
39
 
22
40
  REQUIREMENTS = {
23
41
  "Site": {
@@ -69,30 +87,6 @@ LOOKUPS = {
69
87
  MODEL_KEY = 'landCover'
70
88
 
71
89
  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
90
  SITE_TYPES = {
97
91
  SiteSiteType.CROPLAND.value,
98
92
  SiteSiteType.FOREST.value,
@@ -100,8 +94,7 @@ SITE_TYPES = {
100
94
  SiteSiteType.PERMANENT_PASTURE.value
101
95
  }
102
96
  DEFAULT_WINDOW_IN_YEARS = 20
103
- IPCC_LAND_USE_CATEGORY_ANNUAL = "Annual crops"
104
- IPCC_LAND_USE_CATEGORY_PERENNIAL = "Perennial crops"
97
+ DATE_TOLERANCE_IN_YEARS = 2
105
98
  OUTPUT_SIGNIFICANT_DIGITS = 3
106
99
 
107
100
 
@@ -127,32 +120,6 @@ def site_area_sum_to_100(dict_of_percentages: dict):
127
120
  math.isclose(sum(dict_of_percentages.values()), 0.0, rel_tol=0.01))
128
121
 
129
122
 
130
- def _lookup_land_use_type(nodes: list) -> str:
131
- """Look up the land use type from a management node."""
132
- return "" if nodes == [] else get_lookup_value(
133
- lookup_term=nodes[0].get("term", {}),
134
- column=LOOKUPS.get("landCover")[1],
135
- model=MODEL,
136
- term=nodes[0].get("term", {})
137
- )
138
-
139
-
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
123
  def get_changes(country_id: str, end_year: int) -> dict:
157
124
  """
158
125
  For each entry in ALL_LAND_USE_TERMS, creates a key: value in output dictionary, also TOTAL
@@ -399,7 +366,7 @@ def _get_complete_faostat_to_crop_mapping() -> dict:
399
366
  get_table_value(lookup, 'termid', crop_term_id, column_name("cropGroupingFaostatArea"))
400
367
  )
401
368
  if key:
402
- mappings[key].append(_crop_ipcc_land_use_category(crop_term_id=crop_term_id, lookup_term_type="crop"))
369
+ mappings[key].append(crop_ipcc_land_use_category(crop_term_id=crop_term_id, lookup_term_type="crop"))
403
370
  return {
404
371
  fao_name: max(set(crop_terms), key=crop_terms.count)
405
372
  for fao_name, crop_terms in mappings.items()
@@ -420,6 +387,18 @@ def _get_harvested_area(country_id: str, year: int, faostat_name: str) -> float:
420
387
  )
421
388
 
422
389
 
390
+ def _get_term_id_for_crop(nodes: set, land_type: str) -> str:
391
+ """Use the original crop term id for permanent/perennial crops and the land use id for other types."""
392
+ result = next(
393
+ (node for node in nodes if crop_ipcc_land_use_category(node[0]) == IPCC_LAND_USE_CATEGORY_PERENNIAL), None
394
+ )
395
+ return (
396
+ # Take first perennial crop - not multi-cropping
397
+ result[0] if land_type == PERMANENT_CROPLAND and result else
398
+ LAND_USE_TERMS_FOR_TRANSFORMATION[land_type][0]
399
+ )
400
+
401
+
423
402
  def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from: dict, start_year: int) -> list:
424
403
  """Creates a list of new management nodes, excluding any dates matching existing ones."""
425
404
  existing_nodes_set = {
@@ -435,7 +414,8 @@ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from
435
414
  "percentage": 0 if ratio == -0.0 else to_precision(
436
415
  number=ratio * 100,
437
416
  digits=OUTPUT_SIGNIFICANT_DIGITS
438
- )
417
+ ),
418
+ "term_id": _get_term_id_for_crop(existing_nodes_set, land_type=land_type)
439
419
  }
440
420
  for land_type, ratio in percentage_transformed_from.items()
441
421
  ]
@@ -443,7 +423,7 @@ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from
443
423
 
444
424
  return [
445
425
  _management(
446
- term_id=LAND_USE_TERMS_FOR_TRANSFORMATION[value.get("land_type")][0],
426
+ term_id=value.get("term_id"),
447
427
  value=value.get("percentage"),
448
428
  start_date=value.get("land_management_key")[1],
449
429
  end_date=value.get("land_management_key")[2]
@@ -527,14 +507,13 @@ def _get_net_expansion_cultivated_vs_harvested(annual_crops_net_expansion, chang
527
507
  return net_expansion_cultivated_vs_harvested
528
508
 
529
509
 
530
- def _should_run_historical_land_use_change(site: dict, land_use_type: str) -> tuple[bool, dict]:
531
- management_nodes = filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
510
+ def _should_run_historical_land_use_change(site: dict, nodes: list, land_use_type: str) -> tuple[bool, dict]:
532
511
  # Assume a single management node for single-cropping.
533
512
  return _should_run_historical_land_use_change_single_crop(
534
513
  site=site,
535
- term=management_nodes[0].get("term", {}),
514
+ term=nodes[0].get("term", {}),
536
515
  country_id=site.get("country", {}).get("@id"),
537
- end_year=int(management_nodes[0].get("endDate")[:4]),
516
+ end_year=int(nodes[0].get("endDate")[:4]),
538
517
  land_use_type=land_use_type
539
518
  )
540
519
 
@@ -698,23 +677,77 @@ def _should_run_historical_land_use_change_single_crop(
698
677
  sum_of_site_areas_is_100
699
678
  ]
700
679
  )
701
- logShouldRun(site, MODEL, term=term.get("@id"), should_run=should_run, key=MODEL_KEY)
680
+ logShouldRun(site, MODEL, term.get("@id"), should_run, model_key=MODEL_KEY)
702
681
 
703
682
  return should_run, site_area
704
683
 
705
684
 
685
+ def _get_land_use_term_from_node(node: dict) -> str:
686
+ return get_lookup_value(
687
+ lookup_term=node.get("term", {}),
688
+ column=LOOKUPS.get("landCover")[1],
689
+ model=MODEL,
690
+ term=node.get("term", {})
691
+ )
692
+
693
+
694
+ def _collect_land_use_types(nodes: list) -> list:
695
+ """Look up the land use type from management nodes."""
696
+ return [
697
+ {
698
+ "id": node.get("term", {}).get("@id"),
699
+ "land-use-type": _get_land_use_term_from_node(node),
700
+ "endDate": node.get("endDate")
701
+ } for node in nodes
702
+ ]
703
+
704
+
705
+ def _no_prior_land_cover_data(nodes: list, end_date: str) -> bool:
706
+ target_date = (
707
+ datetime.strptime(end_date, DatestrFormat.YEAR_MONTH_DAY.value)
708
+ - timedelta(days=DEFAULT_WINDOW_IN_YEARS * DAYS_IN_YEAR)
709
+ )
710
+ previous_nodes = [
711
+ node for node in nodes
712
+ if abs(_node_date(node) - target_date) < timedelta(days=DATE_TOLERANCE_IN_YEARS * DAYS_IN_YEAR)
713
+ ]
714
+ return len(previous_nodes) == 0
715
+
716
+
706
717
  def _should_run(site: dict) -> tuple[bool, dict]:
707
718
  management_nodes = filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
708
- land_use_type = _lookup_land_use_type(nodes=management_nodes)
709
- should_run_result, site_area = (
710
- (False, {}) if land_use_type not in {ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE}
711
- else _should_run_historical_land_use_change(
712
- site=site,
713
- land_use_type=land_use_type
714
- )
719
+ summarised_nodes = _collect_land_use_types(management_nodes)
720
+ allowed_land_use_types = [ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE]
721
+ relevant_nodes = sorted(
722
+ [
723
+ node for node in summarised_nodes
724
+ if node["land-use-type"] in allowed_land_use_types
725
+ ],
726
+ key=lambda n: n.get("endDate")
715
727
  )
728
+ land_use_type = relevant_nodes[0].get("land-use-type") if relevant_nodes else None
716
729
 
717
- return should_run_result, site_area
730
+ has_no_prior_land_cover_data = _no_prior_land_cover_data(
731
+ nodes=management_nodes,
732
+ end_date=relevant_nodes[-1:][0].get("endDate")
733
+ ) if relevant_nodes else None
734
+
735
+ should_run_nodes, site_area = _should_run_historical_land_use_change(
736
+ site=site,
737
+ nodes=management_nodes,
738
+ land_use_type=land_use_type
739
+ ) if all([land_use_type, has_no_prior_land_cover_data]) else (False, {})
740
+
741
+ logRequirements(site, model=MODEL, model_key=MODEL_KEY,
742
+ has_management_nodes=bool(management_nodes),
743
+ land_use_type=land_use_type,
744
+ allowed_land_use_types=';'.join(allowed_land_use_types),
745
+ has_no_prior_land_cover_data=has_no_prior_land_cover_data,
746
+ summarised_nodes=log_as_table(summarised_nodes))
747
+
748
+ should_run = all([land_use_type, has_no_prior_land_cover_data, should_run_nodes])
749
+ logShouldRun(site, MODEL, None, should_run, model_key=MODEL_KEY)
750
+ return should_run_nodes, site_area
718
751
 
719
752
 
720
753
  def run(site: dict) -> list:
@@ -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
+ )