hestia-earth-models 0.73.7__py3-none-any.whl → 0.74.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.

Potentially problematic release.


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

Files changed (99) 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 +121 -29
  5. hestia_earth/models/config/ImpactAssessment.json +240 -200
  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/dammgen2009/noxToAirExcreta.py +11 -9
  12. hestia_earth/models/data/ecoinventV3/__init__.py +8 -26
  13. hestia_earth/models/ecoalimV9/cycle.py +51 -45
  14. hestia_earth/models/ecoalimV9/impact_assessment.py +63 -45
  15. hestia_earth/models/ecoalimV9/utils.py +21 -15
  16. hestia_earth/models/ecoinventV3/__init__.py +8 -140
  17. hestia_earth/models/ecoinventV3/cycle.py +140 -0
  18. hestia_earth/models/ecoinventV3/utils.py +28 -1
  19. hestia_earth/models/ecoinventV3AndEmberClimate/__init__.py +8 -137
  20. hestia_earth/models/ecoinventV3AndEmberClimate/cycle.py +144 -0
  21. hestia_earth/models/emepEea2019/n2OToAirFuelCombustionDirect.py +2 -2
  22. hestia_earth/models/emepEea2019/utils.py +2 -3
  23. hestia_earth/models/environmentalFootprintV3_1/environmentalFootprintSingleOverallScore.py +5 -7
  24. hestia_earth/models/frischknechtEtAl2000/ionisingRadiationKbqU235Eq.py +41 -43
  25. hestia_earth/models/geospatialDatabase/awareWaterBasinId.py +2 -2
  26. hestia_earth/models/geospatialDatabase/awareWaterBasinId_v1.py +45 -0
  27. hestia_earth/models/hestia/default_emissions.py +7 -7
  28. hestia_earth/models/hestia/default_resourceUse.py +7 -6
  29. hestia_earth/models/hestia/landCover.py +110 -12
  30. hestia_earth/models/hestia/seed_emissions.py +7 -3
  31. hestia_earth/models/hestia/utils.py +1 -0
  32. hestia_earth/models/hestia/waterSalinity.py +2 -3
  33. hestia_earth/models/impact_assessment/emissions.py +3 -5
  34. hestia_earth/models/ipcc2019/biocharOrganicCarbonPerHa.py +9 -3
  35. hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChange.py +1 -5
  36. hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChange.py +1 -5
  37. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +1 -33
  38. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChange.py +1 -5
  39. hestia_earth/models/ipcc2019/n2OToAirAquacultureSystemsIndirect.py +44 -0
  40. hestia_earth/models/ipcc2019/n2OToAirCropResidueBurningIndirect.py +43 -0
  41. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionIndirect.py +13 -70
  42. hestia_earth/models/ipcc2019/n2OToAirExcretaIndirect.py +13 -70
  43. hestia_earth/models/ipcc2019/n2OToAirFuelCombustionIndirect.py +43 -0
  44. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +13 -70
  45. hestia_earth/models/ipcc2019/n2OToAirNaturalVegetationBurningIndirect.py +43 -0
  46. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +13 -70
  47. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilBurningIndirect.py +43 -0
  48. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationIndirect.py +43 -0
  49. hestia_earth/models/ipcc2019/n2OToAir_indirect_emissions_utils.py +112 -0
  50. hestia_earth/models/ipcc2019/utils.py +0 -25
  51. hestia_earth/models/jarvisAndPain1994/n2ToAirExcreta.py +11 -9
  52. hestia_earth/models/linkedImpactAssessment/emissions.py +25 -16
  53. hestia_earth/models/linkedImpactAssessment/utils.py +5 -1
  54. hestia_earth/models/log.py +8 -3
  55. hestia_earth/models/mocking/search-results.json +1670 -1666
  56. hestia_earth/models/utils/__init__.py +3 -0
  57. hestia_earth/models/utils/background_emissions.py +121 -14
  58. hestia_earth/models/utils/blank_node.py +1 -11
  59. hestia_earth/models/utils/emission.py +18 -8
  60. hestia_earth/models/utils/feedipedia.py +2 -2
  61. hestia_earth/models/utils/impact_assessment.py +4 -6
  62. hestia_earth/models/utils/indicator.py +8 -1
  63. hestia_earth/models/utils/lookup.py +30 -18
  64. hestia_earth/models/utils/productivity.py +1 -1
  65. hestia_earth/models/version.py +1 -1
  66. hestia_earth/orchestrator/log.py +8 -3
  67. hestia_earth/orchestrator/strategies/merge/merge_list.py +41 -54
  68. {hestia_earth_models-0.73.7.dist-info → hestia_earth_models-0.74.0.dist-info}/METADATA +3 -3
  69. {hestia_earth_models-0.73.7.dist-info → hestia_earth_models-0.74.0.dist-info}/RECORD +99 -75
  70. tests/models/aware2_0/__init__.py +0 -0
  71. tests/models/aware2_0/test_scarcityWeightedWaterUse.py +58 -0
  72. tests/models/dammgen2009/test_noxToAirExcreta.py +2 -2
  73. tests/models/ecoalimV9/test_cycle.py +1 -1
  74. tests/models/ecoalimV9/test_impact_assessment.py +1 -1
  75. tests/models/ecoinventV3/__init__.py +0 -0
  76. tests/models/{test_ecoinventV3.py → ecoinventV3/test_cycle.py} +5 -5
  77. tests/models/ecoinventV3AndEmberClimate/__init__.py +0 -0
  78. tests/models/{test_ecoinventV3AndEmberClimate.py → ecoinventV3AndEmberClimate/test_cycle.py} +6 -4
  79. tests/models/environmentalFootprintV3_1/test_environmentalFootprintSingleOverallScore.py +2 -2
  80. tests/models/frischknechtEtAl2000/test_ionisingRadiationKbqU235Eq.py +18 -27
  81. tests/models/hestia/test_landCover.py +16 -6
  82. tests/models/ipcc2019/test_biocharOrganicCarbonPerHa.py +2 -1
  83. tests/models/ipcc2019/test_n2OToAirAquacultureSystemsIndirect.py +45 -0
  84. tests/models/ipcc2019/test_n2OToAirCropResidueBurningIndirect.py +45 -0
  85. tests/models/ipcc2019/test_n2OToAirCropResidueDecompositionIndirect.py +6 -32
  86. tests/models/ipcc2019/test_n2OToAirExcretaIndirect.py +6 -32
  87. tests/models/ipcc2019/test_n2OToAirFuelCombustionIndirect.py +45 -0
  88. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserIndirect.py +6 -32
  89. tests/models/ipcc2019/test_n2OToAirNaturalVegetationBurningIndirect.py +45 -0
  90. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserIndirect.py +6 -32
  91. tests/models/ipcc2019/test_n2OToAirOrganicSoilBurningIndirect.py +45 -0
  92. tests/models/ipcc2019/test_n2OToAirOrganicSoilCultivationIndirect.py +45 -0
  93. tests/models/ipcc2019/test_n2OToAir_indirect_emissions_utils.py +19 -0
  94. tests/models/site/pre_checks/test_cache_geospatialDatabase.py +4 -4
  95. tests/models/test_config.py +53 -7
  96. tests/models/utils/test_background_emissions.py +13 -0
  97. {hestia_earth_models-0.73.7.dist-info → hestia_earth_models-0.74.0.dist-info}/LICENSE +0 -0
  98. {hestia_earth_models-0.73.7.dist-info → hestia_earth_models-0.74.0.dist-info}/WHEEL +0 -0
  99. {hestia_earth_models-0.73.7.dist-info → hestia_earth_models-0.74.0.dist-info}/top_level.txt +0 -0
@@ -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,10 +1,9 @@
1
1
  from hestia_earth.schema import EmissionMethodTier
2
2
  from hestia_earth.utils.tools import flatten, safe_parse_float
3
- from hestia_earth.utils.emission import cycle_emissions_in_system_boundary
4
3
 
5
- from hestia_earth.models.log import logRequirements, logShouldRun
4
+ from hestia_earth.models.log import logRequirements, logShouldRun, debugValues
6
5
  from hestia_earth.models.utils import _omit
7
- from hestia_earth.models.utils.emission import _new_emission
6
+ from hestia_earth.models.utils.emission import _new_emission, background_emissions_in_system_boundary
8
7
  from hestia_earth.models.utils.background_emissions import no_gap_filled_background_emissions
9
8
  from hestia_earth.models.utils.term import get_lookup_value
10
9
  from hestia_earth.models.utils.input import unique_background_inputs
@@ -51,10 +50,7 @@ def _default_value(input: dict):
51
50
 
52
51
 
53
52
  def _run_input(cycle: dict):
54
- required_emission_term_ids = [
55
- id for id in cycle_emissions_in_system_boundary(cycle)
56
- if id.endswith('InputsProduction')
57
- ]
53
+ required_emission_term_ids = background_emissions_in_system_boundary(cycle)
58
54
 
59
55
  def run(input: dict):
60
56
  input_term = input.get('input').get('term')
@@ -63,6 +59,10 @@ def _run_input(cycle: dict):
63
59
 
64
60
  for emission_id in required_emission_term_ids:
65
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)
66
66
 
67
67
  return [
68
68
  _emission(term_id, value, input_term) for term_id in required_emission_term_ids
@@ -1,9 +1,9 @@
1
1
  from hestia_earth.schema import IndicatorMethodTier, TermTermType
2
2
  from hestia_earth.utils.tools import flatten, safe_parse_float
3
- from hestia_earth.utils.emission import emissions_in_system_boundary
4
3
 
5
- from hestia_earth.models.log import logRequirements, logShouldRun
4
+ from hestia_earth.models.log import logRequirements, logShouldRun, debugValues
6
5
  from hestia_earth.models.utils import _omit
6
+ from hestia_earth.models.utils.emission import background_emissions_in_system_boundary
7
7
  from hestia_earth.models.utils.indicator import _new_indicator
8
8
  from hestia_earth.models.utils.background_emissions import no_gap_filled_background_emissions
9
9
  from hestia_earth.models.utils.term import get_lookup_value
@@ -54,10 +54,7 @@ def _default_value(input: dict):
54
54
 
55
55
 
56
56
  def _run_input(impact: dict):
57
- required_resourceUse_term_ids = [
58
- id for id in emissions_in_system_boundary(TermTermType.RESOURCEUSE)
59
- if id.endswith('InputsProduction')
60
- ]
57
+ required_resourceUse_term_ids = background_emissions_in_system_boundary(impact, TermTermType.RESOURCEUSE)
61
58
 
62
59
  def run(input: dict):
63
60
  input_term = input.get('input').get('term')
@@ -66,6 +63,10 @@ def _run_input(impact: dict):
66
63
 
67
64
  for emission_id in required_resourceUse_term_ids:
68
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)
69
70
 
70
71
  return [
71
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,
@@ -109,10 +109,11 @@ def _map_group_emissions(group_id: str, required_emission_term_ids: list, emissi
109
109
  missing_emissions = list(filter(lambda v: v not in emission_ids, emissions))
110
110
  return {
111
111
  'group-id': group_id,
112
+ 'is-group-in-system-boundary': group_id in required_emission_term_ids,
112
113
  'total-emissions': len(emissions),
113
114
  'included-emissions': len(included_emissions),
114
115
  'missing-emissions': '-'.join(missing_emissions),
115
- 'is-valid': len(emissions) == len(included_emissions)
116
+ 'has-all-emissions': len(emissions) == len(included_emissions)
116
117
  }
117
118
 
118
119
 
@@ -140,7 +141,10 @@ def _filter_emissions(cycle: dict):
140
141
  for group_id in group_ids
141
142
  ]
142
143
  # only keep groups that have all emissions present in the Cycle
143
- valid_groups = list(filter(lambda group: group.get('is-valid'), emissions_per_group))
144
+ valid_groups = list(filter(lambda group: all([
145
+ group.get('has-all-emissions'),
146
+ group.get('is-group-in-system-boundary')
147
+ ]), emissions_per_group))
144
148
  valid_group_ids = set([v.get('group-id') for v in valid_groups])
145
149
 
146
150
  # finally, only return emissions which groups are valid
@@ -283,7 +287,7 @@ def _should_run(cycle: dict):
283
287
  # log failed emissions to show in the logs
284
288
  for group in emissions_per_group:
285
289
  emission_id = group.get('group-id')
286
- if not group.get('is-valid') or not should_run:
290
+ if not group.get('has-all-emissions') or not should_run:
287
291
  logRequirements(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY, emission_id=emission_id,
288
292
  **group,
289
293
  **_omit(seed_input, 'input'),
@@ -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),
@@ -2,6 +2,7 @@ from hestia_earth.utils.tools import list_sum
2
2
  from hestia_earth.utils.emission import cycle_emissions_in_system_boundary
3
3
 
4
4
  from hestia_earth.models.log import logRequirements, logShouldRun
5
+ from hestia_earth.models.utils import _include
5
6
  from hestia_earth.models.utils.impact_assessment import get_product, convert_value_from_cycle
6
7
  from hestia_earth.models.utils.indicator import _new_indicator
7
8
  from . import MODEL
@@ -44,11 +45,8 @@ def _indicator(impact_assessment: dict, product: dict):
44
45
 
45
46
  if len(emission.get('inputs', [])):
46
47
  indicator['inputs'] = emission['inputs']
47
- if emission.get('operation'):
48
- indicator['operation'] = emission.get('operation')
49
- if emission.get('transformation'):
50
- indicator['transformation'] = emission.get('transformation')
51
- return indicator
48
+
49
+ return indicator | _include(emission, ['animals', 'operation', 'transformation', 'country', 'key'])
52
50
  return run
53
51
 
54
52
 
@@ -57,12 +57,16 @@ RETURNS = {
57
57
  "statsDefinition": "simulated",
58
58
  "observations": "",
59
59
  "dates": "",
60
+ "depthUpper": "",
61
+ "depthLower": "",
60
62
  "methodClassification": "tier 1 model"
61
63
  }]
62
64
  }
63
65
  TERM_ID = 'biocharOrganicCarbonPerHa'
64
66
 
65
67
  _ITERATIONS = 1000
68
+ _DEPTH_UPPER = 0
69
+ _DEPTH_LOWER = 30
66
70
  _METHOD_CLASSIFICATION = MeasurementMethodClassification.TIER_1_MODEL.value
67
71
  _STATS_DEFINITION = MeasurementStatsDefinition.SIMULATED.value
68
72
 
@@ -172,7 +176,7 @@ def _compile_inventory(
172
176
  cycle_data = {
173
177
  cycle.get("@id"): {
174
178
  "biochar_nodes": filter_list_term_type(cycle.get("inputs", []), TermTermType.BIOCHAR),
175
- **{field: cycle.get(field) for field in COPY_FIELDS}
179
+ **{k: v for k in COPY_FIELDS if (v := cycle.get(k)) is not None}
176
180
  } for cycle in cycles
177
181
  }
178
182
 
@@ -201,7 +205,7 @@ def _compile_inventory(
201
205
  [
202
206
  {
203
207
  "total_oc": total_oc.get(id, 0),
204
- **{field: data.get(field) for field in COPY_FIELDS}
208
+ **data
205
209
  } for id, data in cycle_data.items()
206
210
  ],
207
211
  include_spillovers=True
@@ -427,9 +431,11 @@ def _measurement(
427
431
  "statsDefinition": statsDefinition,
428
432
  "observations": observations,
429
433
  "dates": dates,
434
+ "depthUpper": _DEPTH_UPPER,
435
+ "depthLower": _DEPTH_LOWER,
430
436
  "methodClassification": _METHOD_CLASSIFICATION
431
437
  }
432
438
  measurement = _new_measurement(TERM_ID, MODEL) | {
433
- key: value for key, value in update_dict.items() if value
439
+ key: value for key, value in update_dict.items() if value is not None
434
440
  }
435
441
  return measurement
@@ -78,9 +78,7 @@ def _emission(
78
78
  min: list[float] = None,
79
79
  max: list[float] = None,
80
80
  statsDefinition: str = None,
81
- observations: list[int] = None,
82
- start_date: str,
83
- end_date: str
81
+ observations: list[int] = None
84
82
  ) -> dict:
85
83
  """
86
84
  Create an emission node based on the provided value and method tier.
@@ -108,8 +106,6 @@ def _emission(
108
106
  "max": max,
109
107
  "statsDefinition": statsDefinition,
110
108
  "observations": observations,
111
- "startDate": start_date,
112
- "endDate": end_date,
113
109
  "methodTier": method_tier.value,
114
110
  }
115
111
  emission = _new_emission(term_id, MODEL) | {