hestia-earth-models 0.65.11__py3-none-any.whl → 0.67.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. hestia_earth/models/cache_sites.py +7 -9
  2. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +23 -54
  3. hestia_earth/models/cml2001Baseline/resourceUseEnergyDepletionDuringCycle.py +152 -0
  4. hestia_earth/models/cml2001Baseline/resourceUseEnergyDepletionInputsProduction.py +40 -0
  5. hestia_earth/models/cml2001Baseline/resourceUseMineralsAndMetalsDuringCycle.py +80 -0
  6. hestia_earth/models/cml2001Baseline/resourceUseMineralsAndMetalsInputsProduction.py +40 -0
  7. hestia_earth/models/config/Cycle.json +34 -16
  8. hestia_earth/models/config/ImpactAssessment.json +1867 -1832
  9. hestia_earth/models/config/Site.json +4 -1
  10. hestia_earth/models/cycle/completeness/freshForage.py +10 -2
  11. hestia_earth/models/cycle/cropResidueManagement.py +3 -1
  12. hestia_earth/models/cycle/input/hestiaAggregatedData.py +13 -10
  13. hestia_earth/models/ecoinventV3/__init__.py +2 -1
  14. hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/__init__.py +4 -3
  15. hestia_earth/models/environmentalFootprintV3_1/environmentalFootprintSingleOverallScore.py +135 -0
  16. hestia_earth/models/environmentalFootprintV3_1/marineEutrophicationPotential.py +36 -0
  17. hestia_earth/models/environmentalFootprintV3_1/scarcityWeightedWaterUse.py +40 -0
  18. hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/soilQualityIndexLandTransformation.py +17 -6
  19. hestia_earth/models/geospatialDatabase/{aware.py → awareWaterBasinId.py} +1 -1
  20. hestia_earth/models/hestia/landCover.py +42 -34
  21. hestia_earth/models/hestia/residueRemoved.py +80 -0
  22. hestia_earth/models/hestia/resourceUse_utils.py +43 -29
  23. hestia_earth/models/impact_assessment/product/value.py +1 -1
  24. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +34 -13
  25. hestia_earth/models/ipcc2019/belowGroundBiomass.py +33 -12
  26. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +17 -8
  27. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +7 -4
  28. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2 -1
  29. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +29 -18
  30. hestia_earth/models/ipcc2019/pastureGrass_utils.py +8 -1
  31. hestia_earth/models/log.py +1 -1
  32. hestia_earth/models/mocking/search-results.json +872 -872
  33. hestia_earth/models/site/defaultMethodClassification.py +9 -2
  34. hestia_earth/models/site/defaultMethodClassificationDescription.py +4 -2
  35. hestia_earth/models/site/management.py +48 -30
  36. hestia_earth/models/site/pre_checks/cache_geospatialDatabase.py +19 -14
  37. hestia_earth/models/utils/__init__.py +6 -0
  38. hestia_earth/models/utils/aggregated.py +13 -10
  39. hestia_earth/models/utils/array_builders.py +4 -3
  40. hestia_earth/models/utils/blank_node.py +23 -13
  41. hestia_earth/models/utils/lookup.py +4 -2
  42. hestia_earth/models/utils/property.py +5 -2
  43. hestia_earth/models/version.py +1 -1
  44. hestia_earth/orchestrator/log.py +11 -0
  45. hestia_earth/orchestrator/models/__init__.py +8 -3
  46. hestia_earth/orchestrator/strategies/merge/merge_list.py +17 -6
  47. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/METADATA +1 -1
  48. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/RECORD +86 -69
  49. tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +51 -87
  50. tests/models/cml2001Baseline/test_resourceUseEnergyDepletionDuringCycle.py +103 -0
  51. tests/models/cml2001Baseline/test_resourceUseEnergyDepletionInputsProduction.py +23 -0
  52. tests/models/cml2001Baseline/test_resourceUseMineralsAndMetalsDuringCycle.py +58 -0
  53. tests/models/cml2001Baseline/test_resourceUseMineralsAndMetalsInputsProduction.py +23 -0
  54. tests/models/environmentalFootprintV3_1/test_environmentalFootprintSingleOverallScore.py +93 -0
  55. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_freshwaterEcotoxicityPotentialCtue.py +6 -5
  56. tests/models/environmentalFootprintV3_1/test_marineEutrophicationPotential.py +27 -0
  57. tests/models/environmentalFootprintV3_1/test_scarcityWeightedWaterUse.py +32 -0
  58. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_soilQualityIndexLandOccupation.py +4 -3
  59. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_soilQualityIndexLandTransformation.py +8 -22
  60. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_soilQualityIndexTotalLandUseEffects.py +4 -4
  61. tests/models/faostat2018/product/test_price.py +1 -1
  62. tests/models/geospatialDatabase/{test_aware.py → test_awareWaterBasinId.py} +1 -1
  63. tests/models/hestia/test_landCover.py +2 -1
  64. tests/models/hestia/test_landTransformation20YearAverageDuringCycle.py +2 -1
  65. tests/models/hestia/test_residueRemoved.py +20 -0
  66. tests/models/impact_assessment/test_emissions.py +0 -1
  67. tests/models/ipcc2019/test_aboveGroundBiomass.py +3 -1
  68. tests/models/ipcc2019/test_belowGroundBiomass.py +4 -2
  69. tests/models/ipcc2019/test_organicCarbonPerHa.py +94 -1
  70. tests/models/site/pre_checks/test_cache_geospatialDatabase.py +22 -0
  71. tests/models/site/test_defaultMethodClassification.py +6 -0
  72. tests/models/site/test_defaultMethodClassificationDescription.py +6 -0
  73. tests/models/site/test_management.py +4 -4
  74. tests/models/test_cache_sites.py +2 -2
  75. tests/models/test_config.py +3 -3
  76. tests/models/test_ecoinventV3.py +0 -1
  77. tests/models/utils/test_array_builders.py +2 -2
  78. tests/orchestrator/strategies/merge/test_merge_list.py +11 -1
  79. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/freshwaterEcotoxicityPotentialCtue.py +0 -0
  80. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/soilQualityIndexLandOccupation.py +0 -0
  81. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/soilQualityIndexTotalLandUseEffects.py +0 -0
  82. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/utils.py +0 -0
  83. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/LICENSE +0 -0
  84. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/WHEEL +0 -0
  85. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/top_level.txt +0 -0
  86. /tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/__init__.py +0 -0
@@ -128,7 +128,7 @@
128
128
  {
129
129
  "key": "awareWaterBasinId",
130
130
  "model": "geospatialDatabase",
131
- "value": "aware",
131
+ "value": "awareWaterBasinId",
132
132
  "runStrategy": "add_key_if_missing",
133
133
  "mergeStrategy": "default",
134
134
  "stage": 1
@@ -406,6 +406,9 @@
406
406
  "value": "management",
407
407
  "runStrategy": "always",
408
408
  "mergeStrategy": "list",
409
+ "mergeArgs": {
410
+ "matchDatesFormat": "%Y"
411
+ },
409
412
  "stage": 2
410
413
  },
411
414
  [
@@ -9,6 +9,7 @@ from hestia_earth.utils.tools import list_sum
9
9
 
10
10
  from hestia_earth.models.log import logRequirements
11
11
  from hestia_earth.models.utils import is_from_model
12
+ from hestia_earth.models.utils.blank_node import get_lookup_value
12
13
  from . import MODEL
13
14
 
14
15
  REQUIREMENTS = {
@@ -32,13 +33,17 @@ RETURNS = {
32
33
  "freshForage": ""
33
34
  }
34
35
  }
36
+ LOOKUPS = {
37
+ "liveAnimal": "isGrazingAnimal",
38
+ "liveAquaticSpecies": "isGrazingAnimal"
39
+ }
35
40
  MODEL_KEY = 'freshForage'
36
41
  ALLOWED_SITE_TYPES = [
37
42
  SiteSiteType.PERMANENT_PASTURE.value
38
43
  ]
39
44
 
40
45
 
41
- def _valid_input(input: dict): return is_from_model(input) and list_sum(input.get('value', [-1])) > 0
46
+ def _valid_input(input: dict): return is_from_model(input) and list_sum(input.get('value', [-1])) >= 0
42
47
 
43
48
 
44
49
  def run(cycle: dict):
@@ -47,7 +52,10 @@ def run(cycle: dict):
47
52
 
48
53
  cycle_has_added_forage_input = any(map(_valid_input, cycle.get('inputs', [])))
49
54
 
50
- animals = cycle.get('animals', [])
55
+ animals = [
56
+ a for a in cycle.get('animals', [])
57
+ if get_lookup_value(a.get('term', {}), 'isGrazingAnimal', model=MODEL, key=MODEL_KEY)
58
+ ]
51
59
  all_animals_have_added_forage_input = bool(animals) and all([
52
60
  any(map(_valid_input, animal.get('inputs', []))) for animal in animals
53
61
  ])
@@ -30,6 +30,8 @@ PRACTICE_IDS = [
30
30
  residueIncorporated.TERM_ID,
31
31
  residueLeftOnField.TERM_ID,
32
32
  residueRemoved.TERM_ID,
33
+ 'residueIncorporatedLessThan30DaysBeforeCultivation',
34
+ 'residueIncorporatedMoreThan30DaysBeforeCultivation',
33
35
  ]
34
36
 
35
37
 
@@ -50,7 +52,7 @@ def _should_run(cycle: dict):
50
52
  ])]
51
53
  missing_practices = [term_id for term_id in practice_ids if term_id not in existing_practices]
52
54
 
53
- should_run = all([sum_practices == 100])
55
+ should_run = all([99.5 <= sum_practices <= 100.5])
54
56
 
55
57
  for term_id in missing_practices:
56
58
  logRequirements(cycle, model=MODEL, term=term_id,
@@ -77,19 +77,22 @@ def _run_seed(cycle: dict, primary_product: dict, seed_input: dict, product_term
77
77
  # to avoid double counting seed => aggregated impact => seed, we need to get the impact of the previous decade
78
78
  # if the data does not exist, use the aggregated impact of generic crop instead
79
79
  date = aggregated_end_date(cycle.get('endDate'))
80
- match_end_date = [
81
- {'match': {'endDate': date - 10}}
82
- ]
80
+ match_end_date = [{'match': {'endDate': date - 10}}]
81
+ default_product = get_generic_crop()
83
82
 
84
- impact = find_closest_impact(cycle, date, {'term': product}, region, country, match_end_date) or \
85
- find_closest_impact(cycle, date, primary_product, region, country, match_end_date) or \
86
- find_closest_impact(cycle, date, {'term': get_generic_crop()}, region, country)
83
+ impact = (
84
+ find_closest_impact(cycle, date, product, region, country, match_end_date) or
85
+ find_closest_impact(cycle, date, primary_product.get('term', {}), region, country, match_end_date) or
86
+ find_closest_impact(cycle, date, default_product, region, country)
87
+ )
87
88
 
89
+ search_by_product_term_id = (product or primary_product or default_product).get('@id')
90
+ search_by_region_id = (region or country or {}).get('@id') or 'region-world'
88
91
  debugValues(cycle, model=MODEL_ID, term=seed_input.get('term', {}).get('@id'), key=MODEL_KEY,
89
- input_region=(region or {}).get('@id'),
90
- input_country=(country or {}).get('@id'),
91
- date=date,
92
- impact=(impact or {}).get('@id'))
92
+ search_by_product_term_id=search_by_product_term_id,
93
+ search_by_region_id=search_by_region_id,
94
+ search_by_end_date=str(date),
95
+ impact_assessment_id_found=(impact or {}).get('@id'))
93
96
 
94
97
  return seed_input | {MODEL_KEY: linked_node(impact), 'impactAssessmentIsProxy': True} if impact else None
95
98
 
@@ -69,7 +69,8 @@ LOOKUPS = {
69
69
  "pesticideAI": "ecoinventMapping",
70
70
  "soilAmendment": "ecoinventMapping",
71
71
  "transport": "ecoinventMapping",
72
- "veterinaryDrugs": "ecoinventMapping"
72
+ "veterinaryDrugs": "ecoinventMapping",
73
+ "feedFoodAdditive": "ecoinventMapping"
73
74
  }
74
75
  MODEL = 'ecoinventV3'
75
76
  MODEL_KEY = 'impactAssessment' # keep to generate entry in "model-links.json"
@@ -1,13 +1,14 @@
1
- from os.path import dirname, abspath
2
1
  import sys
3
2
  from importlib import import_module
3
+ from os.path import dirname, abspath
4
4
 
5
5
  from hestia_earth.models.utils.blank_node import run_if_required
6
6
 
7
7
  CURRENT_DIR = dirname(abspath(__file__)) + '/'
8
8
  sys.path.append(CURRENT_DIR)
9
- MODEL = 'environmentalFootprintV3'
10
- PKG = '.'.join(['hestia_earth', 'models', MODEL])
9
+ MODEL = 'environmentalFootprintV3-1'
10
+ MODEL_FOLDER = MODEL.replace('-', '_')
11
+ PKG = '.'.join(['hestia_earth', 'models', MODEL_FOLDER])
11
12
 
12
13
 
13
14
  def run(model: str, data): return run_if_required(MODEL, model, data, import_module(f".{model}", package=PKG))
@@ -0,0 +1,135 @@
1
+ """
2
+ The inputs and outputs from the life cycle inventory are aggregated in 16 midpoint
3
+ characterised impact categories. These impact categories are then normalised (i.e., the results are divided by
4
+ the overall inventory of a reference unit, e.g., the entire world, to convert the characterised impact categories in
5
+ relative shares of the impacts of the analysed system) and weighted (i.e., each impact category is multiplied by
6
+ a weighting factor to reflect their perceived relative importance). The weighted impact categories can then be
7
+ summed to obtain the EF single overall score. The number and the name of the impact categories is the same
8
+ in EF3.0 and EF3.1.
9
+ """
10
+ from typing import List, Optional, Tuple
11
+
12
+ from hestia_earth.schema import TermTermType
13
+ from hestia_earth.utils.lookup import get_table_value, download_lookup, column_name
14
+ from hestia_earth.utils.model import filter_list_term_type
15
+ from hestia_earth.utils.tools import list_sum
16
+
17
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table, debugMissingLookup
18
+ from hestia_earth.models.utils.indicator import _new_indicator
19
+ from hestia_earth.models.utils.lookup import _node_value
20
+ from . import MODEL
21
+
22
+ REQUIREMENTS = {
23
+ "ImpactAssessment": {
24
+ "impacts": [
25
+ {
26
+ "@type": "Indicator",
27
+ "value": "",
28
+ "term.name": "PEF indicators only"
29
+ }
30
+ ]
31
+ }
32
+ }
33
+
34
+ LOOKUPS = {
35
+ "@doc": "Normalisation factors in PEF v3.1 are calculated using a Global population number of 6,895,889,018",
36
+ "characterisedIndicator": ["pefTerm-normalisation-v3_1", "pefTerm-weighing-v3_1"]
37
+ }
38
+
39
+ RETURNS = {
40
+ "Indicator": {
41
+ "value": ""
42
+ }
43
+ }
44
+
45
+ TERM_ID = 'environmentalFootprintSingleOverallScore'
46
+
47
+ normalisation_column = LOOKUPS['characterisedIndicator'][0]
48
+ weighing_column = LOOKUPS['characterisedIndicator'][1]
49
+
50
+
51
+ def _is_a_PEF_indicator(indicator_id) -> bool:
52
+ return (_get_factor(indicator_id, normalisation_column) not in [None, 0, 0.0] and
53
+ _get_factor(indicator_id, weighing_column) not in [None, 0, 0.0])
54
+
55
+
56
+ def _get_factor(indicator_id: str, column) -> Optional[float]:
57
+ factor = get_table_value(download_lookup(f"{list(LOOKUPS.keys())[1]}.csv", keep_in_memory=True),
58
+ 'termid', indicator_id, column_name(column))
59
+ if factor is None:
60
+ debugMissingLookup(f"{list(LOOKUPS.keys())[1]}.csv", 'termid', indicator_id, column, None, model=MODEL,
61
+ term=TERM_ID)
62
+ return float(factor) if factor is not None else None
63
+
64
+
65
+ def _normalise(indicator: dict) -> Optional[float]:
66
+ return (_node_value(indicator) / _get_factor(indicator['term']['@id'], normalisation_column)) \
67
+ if (_node_value(indicator) is not None and
68
+ _get_factor(indicator['term']['@id'], normalisation_column) not in [None, 0, 0.0]) else None
69
+
70
+
71
+ def _weighted_normalise(indicator: dict) -> Optional[float]:
72
+ return (_normalise(indicator) * (_get_factor(indicator['term']['@id'], weighing_column) / 100)
73
+ ) if (_normalise(indicator) is not None and
74
+ _get_factor(indicator['term']['@id'], weighing_column) not in [None, 0, 0.0]) else None
75
+
76
+
77
+ def _indicator(value: float) -> dict:
78
+ indicator = _new_indicator(TERM_ID, MODEL)
79
+ indicator['value'] = value
80
+ return indicator
81
+
82
+
83
+ def _run(indicators: List[dict]):
84
+ return _indicator(value=list_sum([indicator["weighted-normalised"] for indicator in indicators]))
85
+
86
+
87
+ def _valid_indicator(indicator: Optional[dict]) -> bool:
88
+ return all([indicator is not None,
89
+ isinstance(_node_value(indicator), (int, float)),
90
+ _node_value(indicator) is not None,
91
+ _is_a_PEF_indicator(indicator.get('term', {}).get('@id', ''))])
92
+
93
+
94
+ def _should_run(impact_assessment: dict) -> Tuple[bool, list[dict]]:
95
+ indicators = [
96
+ indicator for indicator in
97
+ filter_list_term_type(impact_assessment.get('impacts', []), TermTermType.CHARACTERISEDINDICATOR)
98
+ if _is_a_PEF_indicator(indicator.get('term', {}).get('@id', ''))
99
+ ]
100
+
101
+ has_pef_indicators = bool(indicators)
102
+
103
+ processed_indicators = [{
104
+ "indicator": indicator,
105
+ "valid-indicator": _valid_indicator(indicator),
106
+ "one-indicator-for-category": sum(1 for i in indicators if i['term']['@id'] == indicator['term']['@id']) == 1,
107
+ "indicator-pef-category": indicator['term']['@id'],
108
+ "value": _node_value(indicator),
109
+ "normalised": _normalise(indicator),
110
+ "normalisation-used": _get_factor(indicator['term']['@id'], normalisation_column),
111
+ "weighted-normalised": _weighted_normalise(indicator),
112
+ "weighting-used": _get_factor(indicator['term']['@id'], weighing_column),
113
+ }
114
+ for indicator in indicators
115
+ ]
116
+
117
+ no_duplicate_indicators = all([indicator['one-indicator-for-category'] for indicator in processed_indicators])
118
+ valid_indicators = [indicator for indicator in processed_indicators if indicator['valid-indicator']]
119
+ all_indicators_valid = all([indicator['valid-indicator'] for indicator in processed_indicators])
120
+
121
+ logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
122
+ has_pef_indicators=has_pef_indicators,
123
+ no_duplicate_indicators=no_duplicate_indicators,
124
+ all_indicators_valid=all_indicators_valid,
125
+ processed_indicators=log_as_table(processed_indicators),
126
+ )
127
+
128
+ should_run = all([has_pef_indicators, all_indicators_valid, no_duplicate_indicators])
129
+ logShouldRun(impact_assessment, MODEL, TERM_ID, should_run)
130
+ return should_run, valid_indicators
131
+
132
+
133
+ def run(impact_assessment: dict):
134
+ should_run, indicators = _should_run(impact_assessment)
135
+ return _run(indicators) if should_run else None
@@ -0,0 +1,36 @@
1
+ from hestia_earth.models.log import logRequirements, logShouldRun
2
+ from hestia_earth.models.utils.impact_assessment import impact_emission_lookup_value
3
+ from hestia_earth.models.utils.indicator import _new_indicator
4
+ from . import MODEL
5
+
6
+ REQUIREMENTS = {
7
+ "ImpactAssessment": {
8
+ "emissionsResourceUse": [{"@type": "Indicator", "value": "", "term.termType": "emission"}]
9
+ }
10
+ }
11
+
12
+ RETURNS = {
13
+ "Indicator": {
14
+ "value": ""
15
+ }
16
+ }
17
+
18
+ LOOKUPS = {
19
+ "emission": "nEqMarineEutrophicationEnvironmentalFootprintV3"
20
+ }
21
+
22
+ TERM_ID = 'marineEutrophicationPotential'
23
+
24
+
25
+ def _indicator(value: float):
26
+ indicator = _new_indicator(TERM_ID, MODEL)
27
+ indicator['value'] = value
28
+ return indicator
29
+
30
+
31
+ def run(impact_assessment: dict):
32
+ value = impact_emission_lookup_value(MODEL, TERM_ID, impact_assessment, LOOKUPS['emission'])
33
+ logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
34
+ value=value)
35
+ logShouldRun(impact_assessment, MODEL, TERM_ID, value is not None)
36
+ return _indicator(value) if value is not None else None
@@ -0,0 +1,40 @@
1
+ from hestia_earth.models.log import logRequirements, logShouldRun
2
+ from . import MODEL
3
+ from ..utils.impact_assessment import impact_country_value
4
+ from ..utils.indicator import _new_indicator
5
+
6
+ REQUIREMENTS = {
7
+ "ImpactAssessment": {
8
+ "emissionsResourceUse": [{"@type": "Indicator",
9
+ "term.@id": "freshwaterWithdrawalsDuringCycle",
10
+ "value": ""
11
+ }],
12
+ "optional": {"country": {"@type": "Term", "termType": "region"}}
13
+ }
14
+ }
15
+
16
+ LOOKUPS = {
17
+ "region-resourceUse-environmentalFootprintV31WaterUse": ""
18
+ }
19
+
20
+ RETURNS = {
21
+ "Indicator": {
22
+ "value": ""
23
+ }
24
+ }
25
+ TERM_ID = 'scarcityWeightedWaterUse'
26
+
27
+
28
+ def _indicator(value: float):
29
+ indicator = _new_indicator(TERM_ID, MODEL)
30
+ indicator['value'] = value
31
+ return indicator
32
+
33
+
34
+ def run(impact_assessment: dict):
35
+ value = impact_country_value(MODEL, TERM_ID, impact_assessment, f"{list(LOOKUPS.keys())[0]}.csv",
36
+ country_fallback=True)
37
+ logRequirements(impact_assessment, model=MODEL, term=TERM_ID,
38
+ value=value)
39
+ logShouldRun(impact_assessment, MODEL, TERM_ID, value is not None)
40
+ return None if value is None else _indicator(value)
@@ -25,14 +25,25 @@ REQUIREMENTS = {
25
25
  {
26
26
  "@type": "Indicator",
27
27
  "term.termType": "resourceUse",
28
- "term.@id": ["landTransformation20YearAverageInputsProduction",
29
- "landTransformation20YearAverageDuringCycle"],
28
+ "term.@id": "landTransformation20YearAverageDuringCycle",
30
29
  "value": "> 0",
31
30
  "landCover": {"@type": "Term", "term.termType": "landCover"},
32
31
  "previousLandCover": {"@type": "Term", "term.termType": "landCover"}
33
32
  }
34
33
  ],
35
- "optional": {"country": {"@type": "Term", "termType": "region"}}
34
+ "optional": {
35
+ "country": {"@type": "Term", "termType": "region"},
36
+ "emissionsResourceUse": [
37
+ {
38
+ "@type": "Indicator",
39
+ "term.termType": "resourceUse",
40
+ "term.@id": "landTransformation20YearAverageInputsProduction",
41
+ "value": "> 0",
42
+ "landCover": {"@type": "Term", "term.termType": "landCover"},
43
+ "previousLandCover": {"@type": "Term", "term.termType": "landCover"}
44
+ }
45
+ ]
46
+ }
36
47
  }
37
48
  }
38
49
 
@@ -76,7 +87,8 @@ def _run(transformations: List[dict]):
76
87
 
77
88
 
78
89
  def _is_valid_indicator(indicator: dict) -> bool:
79
- return indicator['term']['@id'] in REQUIREMENTS['ImpactAssessment']['emissionsResourceUse'][0]['term.@id']
90
+ return indicator['term']['@id'] in ["landTransformation20YearAverageInputsProduction",
91
+ "landTransformation20YearAverageDuringCycle"]
80
92
 
81
93
 
82
94
  def _should_run(impact_assessment: dict) -> Tuple[bool, list]:
@@ -144,8 +156,7 @@ def _should_run(impact_assessment: dict) -> Tuple[bool, list]:
144
156
  found_transformations=log_as_table(found_transformations_with_coefficient)
145
157
  )
146
158
 
147
- should_run = has_land_transformation_indicators is False or all([has_land_transformation_indicators,
148
- all_transformations_are_valid])
159
+ should_run = all([has_land_transformation_indicators, all_transformations_are_valid])
149
160
 
150
161
  logShouldRun(impact_assessment, MODEL, TERM_ID, should_run)
151
162
  return should_run, valid_transformations_with_coef
@@ -19,7 +19,7 @@ REQUIREMENTS = {
19
19
  RETURNS = {
20
20
  "The AWARE water basin identifier as a `string`": ""
21
21
  }
22
- MODEL_KEY = 'aware'
22
+ MODEL_KEY = 'awareWaterBasinId'
23
23
  EE_PARAMS = {
24
24
  'collection': 'AWARE',
25
25
  'ee_type': 'vector',
@@ -1,8 +1,8 @@
1
1
  """
2
2
  Land Cover
3
3
 
4
- This model calculates historic land use change over a twenty-year period, extending the
5
- functionality of the Blonk model.
4
+ This model calculates historic land use change over a twenty-year period,
5
+ extending the functionality of the Blonk model.
6
6
  """
7
7
  import math
8
8
  from collections import defaultdict
@@ -10,15 +10,16 @@ from datetime import datetime, timedelta
10
10
 
11
11
  from hestia_earth.schema import SiteSiteType, TermTermType
12
12
  from hestia_earth.utils.lookup import (
13
- download_lookup, get_table_value, column_name,
14
- extract_grouped_data_closest_date, _is_missing_value, extract_grouped_data
13
+ download_lookup, get_table_value, column_name, _is_missing_value, extract_grouped_data
15
14
  )
16
15
  from hestia_earth.utils.model import filter_list_term_type
17
- from hestia_earth.utils.tools import safe_parse_float, to_precision, non_empty_value
16
+ from hestia_earth.utils.tools import safe_parse_float, to_precision
18
17
 
19
18
  from hestia_earth.models.log import logRequirements, log_as_table, logShouldRun
19
+ from hestia_earth.models.utils.constant import DAYS_IN_YEAR
20
20
  from hestia_earth.models.utils.management import _new_management
21
21
  from hestia_earth.models.utils.term import get_lookup_value
22
+ from hestia_earth.models.utils.blank_node import _node_date, DatestrFormat, _gapfill_datestr, DatestrGapfillMode
22
23
  from .utils import (
23
24
  IPCC_LAND_USE_CATEGORY_ANNUAL,
24
25
  IPCC_LAND_USE_CATEGORY_PERENNIAL,
@@ -34,8 +35,6 @@ from .utils import (
34
35
  crop_ipcc_land_use_category,
35
36
  )
36
37
  from . import MODEL
37
- from ..utils.blank_node import _node_date, DatestrFormat, _gapfill_datestr, DatestrGapfillMode
38
- from ..utils.constant import DAYS_IN_YEAR
39
38
 
40
39
  REQUIREMENTS = {
41
40
  "Site": {
@@ -51,7 +50,10 @@ REQUIREMENTS = {
51
50
  "@type": "Management",
52
51
  "value": "",
53
52
  "term.termType": "landCover",
54
- "endDate": ""
53
+ "or": {
54
+ "startDate": "",
55
+ "endDate": ""
56
+ }
55
57
  }
56
58
  ]
57
59
  }
@@ -128,7 +130,7 @@ def _should_group_landCover(term: dict):
128
130
  )
129
131
 
130
132
 
131
- def get_changes(country_id: str, end_year: int) -> dict:
133
+ def get_changes(country_id: str, end_year: int) -> tuple[dict, bool]:
132
134
  """
133
135
  For each entry in ALL_LAND_USE_TERMS, creates a key: value in output dictionary, also TOTAL
134
136
  """
@@ -137,14 +139,18 @@ def get_changes(country_id: str, end_year: int) -> dict:
137
139
  land_use_term: safe_parse_float(
138
140
  extract_grouped_data(
139
141
  get_table_value(lookup, 'termid', country_id, column_name(land_use_term)),
140
- str(end_year))
142
+ str(end_year)),
143
+ default=None
141
144
  )
142
145
  for land_use_term in ALL_LAND_USE_TERMS + [LAND_AREA]
143
146
  }
144
- changes_dict[TOTAL_AGRICULTURAL_CHANGE] = (float(changes_dict.get(TOTAL_CROPLAND, 0))
145
- + float(changes_dict.get(PERMANENT_PASTURE, 0)))
147
+ missing_changes = any(val is None for val in changes_dict.values())
148
+ changes_dict = {k: v if v is not None else 0 for k, v in changes_dict.items()}
149
+ changes_dict[TOTAL_AGRICULTURAL_CHANGE] = (
150
+ float(changes_dict.get(TOTAL_CROPLAND, 0)) + float(changes_dict.get(PERMANENT_PASTURE, 0))
151
+ )
146
152
 
147
- return changes_dict
153
+ return changes_dict, missing_changes
148
154
 
149
155
 
150
156
  def _get_ratio_start_and_end_values(
@@ -156,7 +162,7 @@ def _get_ratio_start_and_end_values(
156
162
  # expansion over twenty years / current area
157
163
  lookup = download_lookup('region-faostatArea.csv')
158
164
  table_value = get_table_value(lookup, 'termid', country_id, column_name(fao_name))
159
- end_value = safe_parse_float(value=extract_grouped_data_closest_date(table_value, end_year), default=None)
165
+ end_value = safe_parse_float(value=extract_grouped_data(table_value, str(end_year)), default=None)
160
166
  return max(0.0, _safe_divide(numerator=expansion, denominator=end_value))
161
167
 
162
168
 
@@ -387,9 +393,9 @@ def _get_harvested_area(country_id: str, year: int, faostat_name: str) -> float:
387
393
  """
388
394
  lookup = download_lookup("region-crop-cropGroupingFaostatProduction-areaHarvested.csv")
389
395
  return safe_parse_float(
390
- value=extract_grouped_data_closest_date(
396
+ value=extract_grouped_data(
391
397
  data=get_table_value(lookup, "termid", country_id, column_name(faostat_name)),
392
- year=year
398
+ key=str(year)
393
399
  ),
394
400
  default=None
395
401
  )
@@ -407,7 +413,9 @@ def _get_term_id_for_crop(nodes: set, land_type: str) -> str:
407
413
  )
408
414
 
409
415
 
410
- def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from: dict, start_year: int) -> list:
416
+ def _run(site: dict, existing_nodes: list, percentage_transformed_from: dict):
417
+ start_year = _get_year_from_landCover(existing_nodes[0]) - DEFAULT_WINDOW_IN_YEARS
418
+
411
419
  """Creates a list of new management nodes, excluding any dates matching existing ones."""
412
420
  existing_nodes_set = {
413
421
  (node.get("term", {}).get("@id", ""), node.get("startDate"), node.get("endDate"))
@@ -429,6 +437,9 @@ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from
429
437
  ]
430
438
  values = [v for v in values if v.get("land_management_key") not in existing_nodes_set]
431
439
 
440
+ for value in values:
441
+ logShouldRun(site, MODEL, value.get("term_id"), True, model_key=MODEL_KEY)
442
+
432
443
  return [
433
444
  _management(
434
445
  term_id=value.get("term_id"),
@@ -515,13 +526,18 @@ def _get_net_expansion_cultivated_vs_harvested(annual_crops_net_expansion, chang
515
526
  return net_expansion_cultivated_vs_harvested
516
527
 
517
528
 
529
+ def _get_year_from_landCover(node: dict):
530
+ date = node.get('startDate') or node.get('endDate')
531
+ return int(date[:4])
532
+
533
+
518
534
  def _should_run_historical_land_use_change(site: dict, nodes: list, land_use_type: str) -> tuple[bool, dict]:
519
535
  # Assume a single management node for single-cropping.
520
536
  return _should_run_historical_land_use_change_single_crop(
521
537
  site=site,
522
538
  term=nodes[0].get("term", {}),
523
539
  country_id=site.get("country", {}).get("@id"),
524
- end_year=int(nodes[0].get("endDate")[:4]),
540
+ end_year=_get_year_from_landCover(nodes[0]),
525
541
  land_use_type=land_use_type
526
542
  )
527
543
 
@@ -535,7 +551,7 @@ def _should_run_historical_land_use_change_single_crop(
535
551
  ) -> tuple[bool, dict]:
536
552
  """Calculate land use change percentages for a single management node/crop."""
537
553
  # (C-H).
538
- changes = get_changes(country_id=country_id, end_year=end_year)
554
+ changes, missing_changes = get_changes(country_id=country_id, end_year=end_year)
539
555
 
540
556
  # (L). Estimate maximum forest loss
541
557
  forest_loss = _estimate_maximum_forest_change(
@@ -665,6 +681,8 @@ def _should_run_historical_land_use_change_single_crop(
665
681
  site_area[land_use_type] = 1 - sum(site_area.values())
666
682
 
667
683
  sum_of_site_areas_is_100 = site_area_sum_to_100(site_area)
684
+ site_type_allowed = site.get("siteType") in SITE_TYPES
685
+
668
686
  logRequirements(
669
687
  log_node=site,
670
688
  model=MODEL,
@@ -672,19 +690,13 @@ def _should_run_historical_land_use_change_single_crop(
672
690
  model_key=MODEL_KEY,
673
691
  land_use_type=land_use_type,
674
692
  country_id=country_id,
693
+ changes=log_as_table(changes),
675
694
  site_area=log_as_table(site_area),
676
- sum_of_site_areas_is_100=sum_of_site_areas_is_100
695
+ sum_of_site_areas_is_100=sum_of_site_areas_is_100,
696
+ site_type_allowed=site_type_allowed
677
697
  )
678
698
 
679
- should_run = all(
680
- [
681
- site.get("siteType"),
682
- country_id,
683
- non_empty_value(term),
684
- site.get("siteType") in SITE_TYPES,
685
- sum_of_site_areas_is_100
686
- ]
687
- )
699
+ should_run = all([not missing_changes, country_id, site_type_allowed, sum_of_site_areas_is_100])
688
700
  logShouldRun(site, MODEL, term.get("@id"), should_run, model_key=MODEL_KEY)
689
701
 
690
702
  return should_run, site_area
@@ -763,8 +775,4 @@ def run(site: dict) -> list:
763
775
  if not _should_group_landCover(node)
764
776
  ]
765
777
  should_run, site_area = _should_run(site=site, management_nodes=management_nodes)
766
- return _run_make_management_nodes(
767
- existing_nodes=management_nodes,
768
- percentage_transformed_from=site_area,
769
- start_year=int(management_nodes[0].get("endDate")[:4]) - DEFAULT_WINDOW_IN_YEARS
770
- ) if should_run else []
778
+ return _run(site, management_nodes, site_area) if should_run else []