hestia-earth-models 0.73.8__py3-none-any.whl → 0.74.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hestia-earth-models might be problematic. Click here for more details.

Files changed (59) hide show
  1. hestia_earth/models/aware/scarcityWeightedWaterUse.py +7 -6
  2. hestia_earth/models/aware2_0/__init__.py +14 -0
  3. hestia_earth/models/aware2_0/scarcityWeightedWaterUse.py +115 -0
  4. hestia_earth/models/config/Cycle.json +5 -3
  5. hestia_earth/models/config/ImpactAssessment.json +1 -1
  6. hestia_earth/models/config/__init__.py +26 -2
  7. hestia_earth/models/cycle/animal/input/hestiaAggregatedData.py +2 -2
  8. hestia_earth/models/cycle/animal/input/properties.py +6 -5
  9. hestia_earth/models/cycle/animal/milkYield.py +8 -3
  10. hestia_earth/models/cycle/utils.py +6 -6
  11. hestia_earth/models/data/ecoinventV3/__init__.py +8 -26
  12. hestia_earth/models/ecoalimV9/cycle.py +26 -10
  13. hestia_earth/models/ecoalimV9/impact_assessment.py +30 -10
  14. hestia_earth/models/ecoalimV9/utils.py +12 -72
  15. hestia_earth/models/ecoinventV3/__init__.py +8 -140
  16. hestia_earth/models/ecoinventV3/cycle.py +140 -0
  17. hestia_earth/models/ecoinventV3/utils.py +28 -1
  18. hestia_earth/models/ecoinventV3AndEmberClimate/__init__.py +8 -137
  19. hestia_earth/models/ecoinventV3AndEmberClimate/cycle.py +144 -0
  20. hestia_earth/models/emepEea2019/utils.py +2 -3
  21. hestia_earth/models/environmentalFootprintV3_1/environmentalFootprintSingleOverallScore.py +5 -7
  22. hestia_earth/models/frischknechtEtAl2000/ionisingRadiationKbqU235Eq.py +41 -43
  23. hestia_earth/models/geospatialDatabase/awareWaterBasinId.py +2 -2
  24. hestia_earth/models/geospatialDatabase/awareWaterBasinId_v1.py +45 -0
  25. hestia_earth/models/hestia/default_emissions.py +5 -1
  26. hestia_earth/models/hestia/default_resourceUse.py +5 -1
  27. hestia_earth/models/hestia/landCover.py +110 -12
  28. hestia_earth/models/hestia/utils.py +1 -0
  29. hestia_earth/models/hestia/waterSalinity.py +2 -3
  30. hestia_earth/models/impact_assessment/emissions.py +1 -1
  31. hestia_earth/models/linkedImpactAssessment/emissions.py +2 -2
  32. hestia_earth/models/log.py +8 -3
  33. hestia_earth/models/mocking/search-results.json +1562 -1558
  34. hestia_earth/models/transformation/product/excreta.py +2 -2
  35. hestia_earth/models/utils/__init__.py +3 -0
  36. hestia_earth/models/utils/background_emissions.py +109 -9
  37. hestia_earth/models/utils/blank_node.py +1 -11
  38. hestia_earth/models/utils/feedipedia.py +2 -2
  39. hestia_earth/models/utils/impact_assessment.py +1 -3
  40. hestia_earth/models/utils/lookup.py +1 -1
  41. hestia_earth/models/version.py +1 -1
  42. hestia_earth/orchestrator/log.py +8 -3
  43. {hestia_earth_models-0.73.8.dist-info → hestia_earth_models-0.74.1.dist-info}/METADATA +2 -2
  44. {hestia_earth_models-0.73.8.dist-info → hestia_earth_models-0.74.1.dist-info}/RECORD +59 -50
  45. tests/models/aware2_0/__init__.py +0 -0
  46. tests/models/aware2_0/test_scarcityWeightedWaterUse.py +58 -0
  47. tests/models/ecoinventV3/__init__.py +0 -0
  48. tests/models/{test_ecoinventV3.py → ecoinventV3/test_cycle.py} +5 -5
  49. tests/models/ecoinventV3AndEmberClimate/__init__.py +0 -0
  50. tests/models/{test_ecoinventV3AndEmberClimate.py → ecoinventV3AndEmberClimate/test_cycle.py} +6 -4
  51. tests/models/environmentalFootprintV3_1/test_environmentalFootprintSingleOverallScore.py +2 -2
  52. tests/models/frischknechtEtAl2000/test_ionisingRadiationKbqU235Eq.py +18 -27
  53. tests/models/hestia/test_landCover.py +16 -6
  54. tests/models/site/pre_checks/test_cache_geospatialDatabase.py +4 -4
  55. tests/models/test_config.py +53 -7
  56. tests/models/{ecoalimV9/test_utils.py → utils/test_background_emissions.py} +2 -2
  57. {hestia_earth_models-0.73.8.dist-info → hestia_earth_models-0.74.1.dist-info}/LICENSE +0 -0
  58. {hestia_earth_models-0.73.8.dist-info → hestia_earth_models-0.74.1.dist-info}/WHEEL +0 -0
  59. {hestia_earth_models-0.73.8.dist-info → hestia_earth_models-0.74.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,144 @@
1
+ from functools import reduce
2
+ from hestia_earth.utils.tools import list_sum, flatten
3
+ from hestia_earth.schema import EmissionMethodTier, TermTermType
4
+ from hestia_earth.utils.blank_node import group_by_keys
5
+
6
+ from hestia_earth.models.log import logShouldRun, logRequirements, debugValues
7
+ from hestia_earth.models.utils.emission import _new_emission
8
+ from hestia_earth.models.utils.background_emissions import get_background_inputs, no_gap_filled_background_emissions
9
+ from hestia_earth.models.utils.completeness import _is_term_type_complete
10
+ from hestia_earth.models.utils.term import get_electricity_grid_mix_terms
11
+ from .utils import get_input_coefficient
12
+ from ..ecoinventV3.utils import get_input_mappings, ecoinvent_values
13
+ from . import MODEL
14
+
15
+ REQUIREMENTS = {
16
+ "Cycle": {
17
+ "completeness.electricityFuel": "True",
18
+ "site": {
19
+ "@type": "Site",
20
+ "country": {"@type": "Term", "termType": "region"}
21
+ },
22
+ "inputs": [{
23
+ "@type": "Input",
24
+ "term.@id": ["electricityGridMarketMix", "electricityGridRenewableMix"],
25
+ "value": "> 0",
26
+ "none": {
27
+ "fromCycle": "True",
28
+ "producedInCycle": "True"
29
+ }
30
+ }],
31
+ "optional": {
32
+ "animals": [{
33
+ "@type": "Animal",
34
+ "inputs": [{
35
+ "@type": "Input",
36
+ "term.@id": ["electricityGridMarketMix", "electricityGridRenewableMix"],
37
+ "value": "> 0",
38
+ "none": {
39
+ "fromCycle": "True",
40
+ "producedInCycle": "True"
41
+ }
42
+ }]
43
+ }]
44
+ }
45
+ }
46
+ }
47
+ RETURNS = {
48
+ "Emission": [{
49
+ "term": "",
50
+ "value": "",
51
+ "methodTier": "background",
52
+ "inputs": "",
53
+ "operation": "",
54
+ "animals": ""
55
+ }]
56
+ }
57
+ LOOKUPS = {
58
+ "emission": "inputProductionGroupId",
59
+ "electricity": "ecoinventMapping",
60
+ "region-ember-energySources": "",
61
+ "ember-ecoinvent-mapping": ["ember", "ecoinventId", "ecoinventName"]
62
+ }
63
+
64
+ MODEL_KEY = 'cycle'
65
+ TIER = EmissionMethodTier.BACKGROUND.value
66
+
67
+
68
+ def _emission(term_id: str, value: float, input: dict):
69
+ emission = _new_emission(term_id, MODEL)
70
+ emission['value'] = [value]
71
+ emission['methodTier'] = TIER
72
+ emission['inputs'] = [input.get('term')]
73
+ if input.get('operation'):
74
+ emission['operation'] = input.get('operation')
75
+ if input.get('animal'):
76
+ emission['animals'] = [input.get('animal')]
77
+ return emission
78
+
79
+
80
+ def _add_emission(cycle: dict, input: dict):
81
+ input_term_id = input.get('term', {}).get('@id')
82
+ operation_term_id = input.get('operation', {}).get('@id')
83
+ animal_term_id = input.get('animal', {}).get('@id')
84
+ # TODO: as inputs are grouped, there could be more than 1 country
85
+ country_id = input.get('country', cycle.get('site', {}).get('country', {})).get('@id')
86
+
87
+ def add(prev: dict, mapping: tuple):
88
+ ecoinventName, _coefficient = mapping
89
+ # recalculate the coefficient using the country and year if it should be included
90
+ coefficient = get_input_coefficient(MODEL, cycle, country_id, ecoinventName) if _coefficient > 0 else 0
91
+ emissions = ecoinvent_values(ecoinventName, TermTermType.EMISSION)
92
+ for emission_term_id, data in emissions:
93
+ # log run on each emission so we know it did run
94
+ logShouldRun(cycle, MODEL, input_term_id, True, methodTier=TIER, emission_id=emission_term_id)
95
+ debugValues(cycle, model=MODEL, term=emission_term_id,
96
+ value=data.get('value'),
97
+ coefficient=coefficient,
98
+ input=input_term_id,
99
+ operation=operation_term_id,
100
+ animal=animal_term_id)
101
+ prev[emission_term_id] = prev.get(emission_term_id, 0) + (data.get('value') * coefficient)
102
+ return prev
103
+ return add
104
+
105
+
106
+ def _run_input(cycle: dict):
107
+ electricity_complete = _is_term_type_complete(cycle, 'electricityFuel')
108
+ no_gap_filled_background_emissions_func = no_gap_filled_background_emissions(cycle)
109
+
110
+ def run(inputs: list):
111
+ input = inputs[0]
112
+ input_value = list_sum(flatten(input.get('value', []) for input in inputs))
113
+ input_term_id = input.get('term', {}).get('@id')
114
+ mappings = get_input_mappings(MODEL, input)
115
+ has_mappings = len(mappings) > 0
116
+
117
+ # skip input that has background emissions we have already gap-filled (model run before)
118
+ has_no_gap_filled_background_emissions = no_gap_filled_background_emissions_func(input)
119
+
120
+ logRequirements(cycle, model=MODEL, term=input_term_id,
121
+ has_ecoinvent_mappings=has_mappings,
122
+ ecoinvent_mappings=';'.join([v[0] for v in mappings]),
123
+ has_no_gap_filled_background_emissions=has_no_gap_filled_background_emissions,
124
+ termType_electricityFuel_complete=electricity_complete,
125
+ input_value=input_value)
126
+
127
+ should_run = all([electricity_complete, has_mappings, has_no_gap_filled_background_emissions, input_value])
128
+ logShouldRun(cycle, MODEL, input_term_id, should_run, methodTier=TIER)
129
+
130
+ grouped_emissions = reduce(_add_emission(cycle, input), mappings, {}) if should_run else {}
131
+ return [
132
+ _emission(term_id, value * input_value, input)
133
+ for term_id, value in grouped_emissions.items()
134
+ ]
135
+ return run
136
+
137
+
138
+ def run(cycle: dict):
139
+ terms = get_electricity_grid_mix_terms()
140
+ inputs = get_background_inputs(cycle)
141
+ # only keep the inputs matching the grid terms
142
+ inputs = [i for i in inputs if i.get('term', {}).get('@id') in terms]
143
+ grouped_inputs = group_by_keys(inputs, ['term', 'operation', 'animal'])
144
+ return flatten(map(_run_input(cycle), grouped_inputs.values()))
@@ -1,12 +1,11 @@
1
- from functools import reduce
2
1
  from hestia_earth.schema import TermTermType, SiteSiteType
3
2
  from hestia_earth.utils.model import filter_list_term_type
4
3
  from hestia_earth.utils.lookup import extract_grouped_data
4
+ from hestia_earth.utils.blank_node import group_by_keys
5
5
  from hestia_earth.utils.tools import list_sum, safe_parse_float
6
6
 
7
7
  from hestia_earth.models.log import logRequirements, logShouldRun
8
8
  from hestia_earth.models.utils.completeness import _is_term_type_complete
9
- from hestia_earth.models.utils.blank_node import group_by_keys
10
9
  from hestia_earth.models.utils.term import get_lookup_value
11
10
  from hestia_earth.models.utils.cycle import get_animals_by_period
12
11
  from hestia_earth.models.utils.emission import _new_emission
@@ -90,7 +89,7 @@ def get_fuel_inputs(term_id: str, cycle: dict, lookup_col: str):
90
89
 
91
90
 
92
91
  def group_fuel_inputs(inputs: list):
93
- return reduce(group_by_keys(['input-id', 'operation-id']), inputs, {}) if len(inputs) > 0 else None
92
+ return group_by_keys(inputs, ['input-id', 'operation-id']) if len(inputs) > 0 else None
94
93
 
95
94
 
96
95
  def _get_emissions_factor(animal: dict, lookup_col: str) -> float:
@@ -88,20 +88,18 @@ def _indicator_factors(impact_assessment: dict, indicator: dict):
88
88
  }
89
89
 
90
90
 
91
- def _map_input_ids(value: dict) -> set[str]:
92
- return set(map(lambda i: i.get('@id'), value.get('inputs', [])))
93
-
94
-
95
91
  def _count_duplicate_indicators(reference_indicator: dict, indicators: list) -> int:
96
92
  """
97
93
  Counts the number of `reference_indicator` indicators found in a list of indicators.
98
- Uses indicator.term.@id and indicator.inputs to determine uniqueness.
94
+ Uses indicator.term.@id and indicator.key to determine uniqueness.
99
95
  """
100
96
  return sum([
101
97
  1
102
98
  for i in indicators
103
- if (i["term"]["@id"] == reference_indicator["term"]["@id"]) and (
104
- _map_input_ids(i) == _map_input_ids(reference_indicator))
99
+ if all([
100
+ i["term"]["@id"] == reference_indicator["term"]["@id"],
101
+ i.get("key", {}).get("@id") == reference_indicator.get("key", {}).get("@id")
102
+ ])
105
103
  ])
106
104
 
107
105
 
@@ -1,11 +1,11 @@
1
- from functools import reduce
2
1
  from hestia_earth.schema import TermTermType
3
2
  from hestia_earth.utils.lookup import get_table_value, download_lookup, column_name
4
3
  from hestia_earth.utils.model import filter_list_term_type
5
4
  from hestia_earth.utils.tools import flatten, list_sum
5
+ from hestia_earth.utils.blank_node import group_by_keys
6
6
 
7
7
  from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
8
- from hestia_earth.models.utils.blank_node import group_by_keys
8
+ from hestia_earth.models.utils import unique_values, _omit, _include
9
9
  from hestia_earth.models.utils.indicator import _new_indicator
10
10
  from hestia_earth.models.utils.lookup import _node_value
11
11
  from . import MODEL
@@ -20,7 +20,7 @@ REQUIREMENTS = {
20
20
  "ionisingCompoundsToWaterInputsProduction",
21
21
  "ionisingCompoundsToSaltwaterInputsProduction"
22
22
  ],
23
- "inputs": [{"@type": "Term", "term.termType": "waste", "term.units": "kg"}]
23
+ "key": {"@type": "Term", "term.termType": "waste", "term.units": "kg"}
24
24
  }]
25
25
  }
26
26
  }
@@ -34,6 +34,7 @@ LOOKUPS = {
34
34
  RETURNS = {
35
35
  "Indicator": [{
36
36
  "value": "",
37
+ "key": "",
37
38
  "inputs": ""
38
39
  }]
39
40
  }
@@ -41,31 +42,32 @@ RETURNS = {
41
42
  TERM_ID = 'ionisingRadiationKbqU235Eq'
42
43
 
43
44
 
44
- def _valid_waste(input: dict) -> bool:
45
- return input.get('units', '').startswith("kg") and input.get('termType', '') == TermTermType.WASTE.value
46
-
47
-
48
- def _valid_emission(emission: dict) -> bool:
49
- return len(emission.get('inputs', [])) == 1 and isinstance(_node_value(emission), (int, float))
50
-
51
-
52
- def _indicator(value: float, input: dict) -> dict:
45
+ def _indicator(value: float, key: dict, inputs: list) -> dict:
53
46
  indicator = _new_indicator(TERM_ID, MODEL)
47
+ indicator['key'] = key
54
48
  indicator['value'] = value
55
- indicator['inputs'] = [input]
49
+ if inputs:
50
+ indicator['inputs'] = inputs
56
51
  return indicator
57
52
 
58
53
 
59
54
  def _run(emissions: list) -> list[dict]:
60
55
  indicators = [
61
- _indicator(value=list_sum([emission['value'] * emission['coefficient'] for emission in emission_group]),
62
- input=emission_group[0]['input'])
63
- for emission_group in reduce(group_by_keys(['input']), emissions, {}).values()
56
+ _indicator(
57
+ value=list_sum([emission['value'] * emission['coefficient'] for emission in emission_group]),
58
+ key=emission_group[0]['key'],
59
+ inputs=unique_values(flatten([emission.get('inputs', []) for emission in emission_group]))
60
+ )
61
+ for emission_group in group_by_keys(emissions, ['key']).values()
64
62
  ]
65
63
 
66
64
  return indicators
67
65
 
68
66
 
67
+ def _valid_key(term: dict) -> bool:
68
+ return term.get('units', '').startswith("kg") and term.get('termType') == TermTermType.WASTE.value
69
+
70
+
69
71
  def _should_run(impact_assessment: dict) -> tuple[bool, list]:
70
72
  emissions = [
71
73
  emission
@@ -75,38 +77,34 @@ def _should_run(impact_assessment: dict) -> tuple[bool, list]:
75
77
 
76
78
  has_emissions = bool(emissions)
77
79
 
78
- emissions_unpacked = flatten(
79
- [
80
- [
81
- {
82
- "input-term-id": input.get('@id'),
83
- "input-term-type": input.get('termType'),
84
- "indicator-term-id": emission['term']['@id'],
85
- "indicator-is-valid": _valid_emission(emission),
86
- "input": input,
87
- "indicator-input-is-valid": _valid_waste(input),
88
- "value": _node_value(emission),
89
- "coefficient": get_table_value(lookup=download_lookup(filename="waste.csv"),
90
- col_match='termid',
91
- col_match_with=input.get('@id'),
92
- col_val=column_name(emission['term']['@id'])) if input else None
93
- } for input in emission['inputs'] or [{}]]
94
- for emission in emissions
95
- ]
96
- )
80
+ emissions_unpacked = [
81
+ {
82
+ "key-term-id": emission['key'].get('@id'),
83
+ "key-term-type": emission['key'].get('termType'),
84
+ "key-is-valid": _valid_key(emission['key']),
85
+ "indicator-term-id": emission['term']['@id'],
86
+ "value": _node_value(emission),
87
+ "coefficient": get_table_value(
88
+ lookup=download_lookup(filename="waste.csv"),
89
+ col_match='termid',
90
+ col_match_with=emission['key'].get('@id'),
91
+ col_val=column_name(emission['term']['@id'])
92
+ )
93
+ } | _include(emission, ['key', 'inputs'])
94
+ for emission in emissions
95
+ if emission.get('key')
96
+ ]
97
97
 
98
98
  valid_emission_with_cf = [
99
99
  em for em in emissions_unpacked if all([
100
100
  em['coefficient'] is not None,
101
- em['indicator-is-valid'] is True,
102
- em['indicator-input-is-valid'] is True
101
+ em['key-is-valid'] is True
103
102
  ])
104
103
  ]
105
104
 
106
- valid_input_requirements = all([
105
+ valid_key_requirements = all([
107
106
  all([
108
- em['indicator-is-valid'],
109
- em['indicator-input-is-valid']
107
+ em['key-is-valid']
110
108
  ])
111
109
  for em in emissions_unpacked
112
110
  ])
@@ -117,12 +115,12 @@ def _should_run(impact_assessment: dict) -> tuple[bool, list]:
117
115
 
118
116
  logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
119
117
  has_emissions=has_emissions,
120
- valid_input_requirements=valid_input_requirements,
118
+ valid_key_requirements=valid_key_requirements,
121
119
  all_emissions_have_known_CF=all_emissions_have_known_cf,
122
- emissions=log_as_table(emissions_unpacked)
120
+ emissions=log_as_table([_omit(v, ['key', 'inputs']) for v in emissions_unpacked])
123
121
  )
124
122
 
125
- should_run = valid_input_requirements
123
+ should_run = all([emissions_unpacked, valid_key_requirements])
126
124
 
127
125
  logShouldRun(impact_assessment, MODEL, TERM_ID, should_run)
128
126
  return should_run, valid_emission_with_cf
@@ -16,9 +16,9 @@ RETURNS = {
16
16
  }
17
17
  MODEL_KEY = 'awareWaterBasinId'
18
18
  EE_PARAMS = {
19
- 'collection': 'AWARE',
19
+ 'collection': 'AWARE_2_0_ids',
20
20
  'ee_type': 'vector',
21
- 'fields': 'Name'
21
+ 'fields': 'Basin_ID'
22
22
  }
23
23
 
24
24
 
@@ -0,0 +1,45 @@
1
+ from hestia_earth.models.log import logRequirements, logShouldRun
2
+ from .utils import download, has_geospatial_data, should_download
3
+ from . import MODEL
4
+
5
+ REQUIREMENTS = {
6
+ "Site": {
7
+ "or": [
8
+ {"latitude": "", "longitude": ""},
9
+ {"boundary": {}},
10
+ {"region": {"@type": "Term", "termType": "region"}}
11
+ ]
12
+ }
13
+ }
14
+ RETURNS = {
15
+ "The AWARE water basin identifier as a `string`": ""
16
+ }
17
+ MODEL_KEY = 'awareWaterBasinId_v1'
18
+ EE_PARAMS = {
19
+ 'collection': 'AWARE',
20
+ 'ee_type': 'vector',
21
+ 'fields': 'Name'
22
+ }
23
+
24
+
25
+ def _download(site: dict):
26
+ return download(MODEL_KEY, site, EE_PARAMS)
27
+
28
+
29
+ def _run(site: dict): return _download(site)
30
+
31
+
32
+ def _should_run(site: dict):
33
+ contains_geospatial_data = has_geospatial_data(site)
34
+ below_max_area_size = should_download(MODEL_KEY, site)
35
+
36
+ logRequirements(site, model=MODEL, model_key=MODEL_KEY,
37
+ contains_geospatial_data=contains_geospatial_data,
38
+ below_max_area_size=below_max_area_size)
39
+
40
+ should_run = all([contains_geospatial_data, below_max_area_size])
41
+ logShouldRun(site, MODEL, None, should_run, model_key=MODEL_KEY)
42
+ return should_run
43
+
44
+
45
+ def run(site: dict): return _run(site) if _should_run(site) else None
@@ -1,7 +1,7 @@
1
1
  from hestia_earth.schema import EmissionMethodTier
2
2
  from hestia_earth.utils.tools import flatten, safe_parse_float
3
3
 
4
- from hestia_earth.models.log import logRequirements, logShouldRun
4
+ from hestia_earth.models.log import logRequirements, logShouldRun, debugValues
5
5
  from hestia_earth.models.utils import _omit
6
6
  from hestia_earth.models.utils.emission import _new_emission, background_emissions_in_system_boundary
7
7
  from hestia_earth.models.utils.background_emissions import no_gap_filled_background_emissions
@@ -59,6 +59,10 @@ def _run_input(cycle: dict):
59
59
 
60
60
  for emission_id in required_emission_term_ids:
61
61
  logShouldRun(cycle, MODEL, term_id, True, methodTier=TIER, model_key=MODEL_KEY, emission_id=emission_id)
62
+ debugValues(cycle, model=MODEL, term=emission_id,
63
+ value=value,
64
+ coefficient=1,
65
+ input=term_id)
62
66
 
63
67
  return [
64
68
  _emission(term_id, value, input_term) for term_id in required_emission_term_ids
@@ -1,7 +1,7 @@
1
1
  from hestia_earth.schema import IndicatorMethodTier, TermTermType
2
2
  from hestia_earth.utils.tools import flatten, safe_parse_float
3
3
 
4
- from hestia_earth.models.log import logRequirements, logShouldRun
4
+ from hestia_earth.models.log import logRequirements, logShouldRun, debugValues
5
5
  from hestia_earth.models.utils import _omit
6
6
  from hestia_earth.models.utils.emission import background_emissions_in_system_boundary
7
7
  from hestia_earth.models.utils.indicator import _new_indicator
@@ -63,6 +63,10 @@ def _run_input(impact: dict):
63
63
 
64
64
  for emission_id in required_resourceUse_term_ids:
65
65
  logShouldRun(impact, MODEL, term_id, True, methodTier=TIER, model_key=MODEL_KEY, emission_id=emission_id)
66
+ debugValues(impact, model=MODEL, term=emission_id,
67
+ value=value,
68
+ coefficient=1,
69
+ input=term_id)
66
70
 
67
71
  return [
68
72
  _indicator(term_id, value, input_term) for term_id in required_resourceUse_term_ids
@@ -29,6 +29,7 @@ from .utils import (
29
29
  TOTAL_AGRICULTURAL_CHANGE,
30
30
  ALL_LAND_USE_TERMS,
31
31
  crop_ipcc_land_use_category,
32
+ LAND_USE_NAMES_FROM_ID
32
33
  )
33
34
  from . import MODEL
34
35
 
@@ -101,7 +102,8 @@ _BUILDING_SITE_TYPES = [
101
102
  _DEFAULT_WINDOW_IN_YEARS = 20
102
103
  _DATE_TOLERANCE_IN_YEARS = 2
103
104
  _OUTPUT_SIGNIFICANT_DIGITS = 3
104
- _ALLOWED_LAND_USE_TYPES = [ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE]
105
+ _ALLOWED_LAND_USE_TYPES = [ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE, TOTAL_CROPLAND]
106
+ _TOP_LEVEL_LAND_USE_TYPE_IDS = {"annualCropland", "permanentCropland", "permanentPasture", "cropland"}
105
107
  _LOOKUP_EXPANSION = "region-crop-cropGroupingFaostatProduction-areaHarvestedUpTo20YearExpansion.csv"
106
108
  _COMPLETE_CHANGES_OTHER_LAND = {
107
109
  OTHER_LAND: 1,
@@ -214,7 +216,7 @@ def _should_group_landCover(management_node: dict):
214
216
  )
215
217
 
216
218
 
217
- def _get_changes(country_id: str, end_year: int) -> tuple[dict, bool]:
219
+ def _get_changes(country_id: str, end_year: int) -> tuple[dict, list]:
218
220
  """
219
221
  For each entry in ALL_LAND_USE_TERMS, creates a key: value in output dictionary, also TOTAL
220
222
  """
@@ -250,6 +252,25 @@ def _get_ratio_start_and_end_values(
250
252
  return max(0.0, _safe_divide(numerator=expansion, denominator=end_value))
251
253
 
252
254
 
255
+ def _get_ratio_between_land_use_types(
256
+ country_id: str,
257
+ end_year: int,
258
+ first_land_use_term: str,
259
+ second_land_use_term: str
260
+ ) -> tuple:
261
+ """Returns a tuple of the values of the two land use terms for the same country and year."""
262
+ return tuple([
263
+ safe_parse_float(value=extract_grouped_data(
264
+ get_region_lookup_value(
265
+ 'region-faostatArea.csv', country_id, land_use_term, model=MODEL, key=MODEL_KEY
266
+ ),
267
+ str(end_year)),
268
+ default=None
269
+ )
270
+ for land_use_term in [first_land_use_term, second_land_use_term]
271
+ ])
272
+
273
+
253
274
  def _estimate_maximum_forest_change(
254
275
  forest_change: float, total_cropland_change: float, pasture_change: float, total_agricultural_change: float
255
276
  ):
@@ -384,7 +405,7 @@ def _estimate_conversions_to_permanent_cropland(
384
405
  forest_loss_to_cropland: float,
385
406
  other_land_loss_to_annual_cropland: float
386
407
  ) -> dict:
387
- """Estimate percentage of land sources when converted to: Annual cropland"""
408
+ """Estimate percentage of land sources when converted to: Permanent cropland"""
388
409
 
389
410
  def conversion_to_permanent_cropland(factor: float):
390
411
  return _safe_divide(
@@ -642,15 +663,92 @@ def _scale_site_area_errors(site_area: dict) -> dict:
642
663
  if negative_errors and abs(negative_errors[0]) < 1 and all([v < 1 for v in site_area.values()]) else site_area
643
664
 
644
665
 
666
+ def _new_landCover_term(new_land_use_term) -> dict:
667
+ return {
668
+ "@id": LAND_USE_TERMS_FOR_TRANSFORMATION[new_land_use_term][0],
669
+ "name": LAND_USE_TERMS_FOR_TRANSFORMATION[new_land_use_term][1],
670
+ "@type": "Term",
671
+ "termType": TermTermType.LANDCOVER.value
672
+ }
673
+
674
+
675
+ def _scaled_value(
676
+ permanent_crops_value: float,
677
+ annual_crops_value: float,
678
+ permanent_crops_factor: float,
679
+ annual_crops_factor: float,
680
+ ):
681
+ total_area = permanent_crops_factor + annual_crops_factor
682
+ permanent_crops_scaled = permanent_crops_value * permanent_crops_factor / total_area
683
+ annual_crops_scaled = annual_crops_value * annual_crops_factor / total_area
684
+ return annual_crops_scaled + permanent_crops_scaled
685
+
686
+
687
+ def _scale_from_annual_and_permanent_results(
688
+ annual_cropland_results: dict,
689
+ permanent_cropland_results: dict,
690
+ annual_cropland_factor: float,
691
+ permanent_cropland_factor: float
692
+ ) -> dict:
693
+ return {
694
+ land_key: _scaled_value(
695
+ permanent_crops_value=permanent_cropland_results[land_key],
696
+ annual_crops_value=land_value,
697
+ permanent_crops_factor=permanent_cropland_factor,
698
+ annual_crops_factor=annual_cropland_factor
699
+ )
700
+ for land_key, land_value in annual_cropland_results.items()
701
+ }
702
+
703
+
645
704
  def _should_run_historical_land_use_change(site: dict, nodes: list, land_use_type: str) -> tuple[bool, dict]:
646
705
  # Assume a single management node for single-cropping.
647
- return _should_run_historical_land_use_change_single_crop(
706
+ return (
707
+ _should_run_historical_land_use_change_total_cropland(site, nodes) if land_use_type == TOTAL_CROPLAND else
708
+ _should_run_historical_land_use_change_single_crop(
709
+ site=site,
710
+ term=nodes[0].get("term", {}),
711
+ country_id=site.get("country", {}).get("@id"),
712
+ end_year=_get_year_from_landCover(nodes[0]),
713
+ land_use_type=land_use_type
714
+ )
715
+ )
716
+
717
+
718
+ def _should_run_historical_land_use_change_total_cropland(site: dict, nodes: list) -> tuple[bool, dict]:
719
+ end_year = _get_year_from_landCover(nodes[0])
720
+ country_id = site.get("country", {}).get("@id")
721
+
722
+ # Run _should_run_historical_land_use_change_single_crop for annual and permanent
723
+ should_run_annual, areas_for_annual_cropland = _should_run_historical_land_use_change_single_crop(
648
724
  site=site,
649
- term=nodes[0].get("term", {}),
650
- country_id=site.get("country", {}).get("@id"),
651
- end_year=_get_year_from_landCover(nodes[0]),
652
- land_use_type=land_use_type
725
+ term=_new_landCover_term(ANNUAL_CROPLAND),
726
+ country_id=country_id,
727
+ end_year=end_year,
728
+ land_use_type=ANNUAL_CROPLAND
653
729
  )
730
+ should_run_permanent, areas_for_permanent_cropland = _should_run_historical_land_use_change_single_crop(
731
+ site=site,
732
+ term=_new_landCover_term(PERMANENT_CROPLAND),
733
+ country_id=country_id,
734
+ end_year=end_year,
735
+ land_use_type=PERMANENT_CROPLAND
736
+ )
737
+ # Get current ratios ("Arable land" vs "Permanent crops")
738
+ annual_cropland_factor, permanent_crops_factor = _get_ratio_between_land_use_types(
739
+ country_id=country_id,
740
+ end_year=end_year,
741
+ first_land_use_term=ANNUAL_CROPLAND,
742
+ second_land_use_term=PERMANENT_CROPLAND
743
+ ) if should_run_annual and should_run_permanent else tuple([0, 0])
744
+ scaled_results = _scale_from_annual_and_permanent_results(
745
+ annual_cropland_results=areas_for_annual_cropland,
746
+ permanent_cropland_results=areas_for_permanent_cropland,
747
+ annual_cropland_factor=annual_cropland_factor,
748
+ permanent_cropland_factor=permanent_crops_factor
749
+ ) if should_run_annual and should_run_permanent else {}
750
+ # Scale results according to current ratios.
751
+ return all([should_run_annual, should_run_permanent]), scaled_results
654
752
 
655
753
 
656
754
  def _should_run_historical_land_use_change_single_crop(
@@ -743,11 +841,11 @@ def _should_run_historical_land_use_change_single_crop(
743
841
 
744
842
  # Cell E8
745
843
  expansion_factor = _get_ratio_start_and_end_values(
746
- expansion=changes[PERMANENT_PASTURE],
747
- fao_name=PERMANENT_PASTURE,
844
+ expansion=changes[LAND_USE_NAMES_FROM_ID[term.get("@id")]],
845
+ fao_name=land_use_type,
748
846
  country_id=country_id,
749
847
  end_year=end_year
750
- ) if land_use_type == PERMANENT_PASTURE else get_ratio_of_expanded_area(
848
+ ) if term.get("@id") in _TOP_LEVEL_LAND_USE_TYPE_IDS else get_ratio_of_expanded_area(
751
849
  country_id=country_id,
752
850
  fao_name=_get_faostat_name(term),
753
851
  end_year=end_year
@@ -774,7 +872,7 @@ def _should_run_historical_land_use_change_single_crop(
774
872
  ) if land_use_type == ANNUAL_CROPLAND else 1
775
873
  )
776
874
 
777
- # E10: Compare changes to annual/perennial cropland from net expansion.
875
+ # E10: Compare changes to annual/permanent cropland from net expansion.
778
876
  net_expansion_cultivated_vs_harvested = _get_net_expansion_cultivated_vs_harvested(
779
877
  annual_crops_net_expansion=annual_crops_net_expansion,
780
878
  changes=changes,
@@ -35,6 +35,7 @@ LAND_USE_TERMS_FOR_TRANSFORMATION = {
35
35
  PERMANENT_PASTURE: ("permanentPasture", "Permanent pasture"),
36
36
  OTHER_LAND: ("otherLand", OTHER_LAND) # Not used yet
37
37
  }
38
+ LAND_USE_NAMES_FROM_ID = {v[0]: k for k, v in LAND_USE_TERMS_FOR_TRANSFORMATION.items()} | {"cropland": TOTAL_CROPLAND}
38
39
 
39
40
 
40
41
  def crop_ipcc_land_use_category(
@@ -1,13 +1,12 @@
1
- from functools import reduce
2
1
  from hestia_earth.schema import MeasurementMethodClassification, TermTermType
3
2
  from hestia_earth.utils.model import filter_list_term_type
4
3
  from hestia_earth.utils.tools import safe_parse_float, list_average, non_empty_list
4
+ from hestia_earth.utils.blank_node import group_by_keys
5
5
 
6
6
  from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
7
7
  from hestia_earth.models.utils.measurement import _new_measurement
8
8
  from hestia_earth.models.utils.site import related_cycles
9
9
  from hestia_earth.models.utils.term import get_lookup_value
10
- from hestia_earth.models.utils.blank_node import group_by_keys
11
10
  from . import MODEL
12
11
 
13
12
  REQUIREMENTS = {
@@ -75,7 +74,7 @@ def _should_run(site: dict):
75
74
 
76
75
  def run(site: dict):
77
76
  should_run, values = _should_run(site)
78
- grouped_values = reduce(group_by_keys(['start-date', 'end-date']), values, {})
77
+ grouped_values = group_by_keys(values, ['start-date', 'end-date'])
79
78
  return non_empty_list([
80
79
  _measurement(
81
80
  list_average([v.get('lookup-value') for v in value if v.get('lookup-value')], default=0),