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
@@ -43,6 +43,9 @@ def _omit(values: dict, keys: list) -> dict: return {k: v for k, v in values.ite
43
43
  def _include(value: dict, keys: list) -> dict: return {k: v for k, v in value.items() if k in keys}
44
44
 
45
45
 
46
+ def unique_values(values: list, key='@id'): return list({v[key]: v for v in values}.values())
47
+
48
+
46
49
  def _run_in_serie(data: dict, models: list): return reduce(lambda prev, model: model(prev), models, data)
47
50
 
48
51
 
@@ -1,16 +1,19 @@
1
- from hestia_earth.schema import TermTermType
1
+ from functools import reduce
2
+ from typing import Callable, Tuple
3
+ from hestia_earth.schema import TermTermType, EmissionMethodTier
4
+ from hestia_earth.utils.lookup import _is_missing_value, lookup_columns
2
5
  from hestia_earth.utils.model import find_term_match, filter_list_term_type
3
- from hestia_earth.utils.tools import flatten
6
+ from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_float
4
7
  from hestia_earth.utils.emission import cycle_emissions_in_system_boundary
5
8
 
6
- from hestia_earth.models.log import logShouldRun
7
- from . import is_from_model
9
+ from hestia_earth.models.log import logShouldRun, logRequirements, debugValues
10
+ from . import is_from_model, _omit
8
11
  from .term import get_lookup_value
9
12
 
10
13
 
11
14
  def _animal_inputs(animal: dict):
12
15
  inputs = animal.get('inputs', [])
13
- return [(input | {'animal': animal.get('term', {})}) for input in inputs]
16
+ return [(input | {'animalId': animal['animalId'], 'animal': animal.get('term', {})}) for input in inputs]
14
17
 
15
18
 
16
19
  def _should_run_input(products: list):
@@ -58,23 +61,127 @@ def no_gap_filled_background_emissions(
58
61
  return check_input
59
62
 
60
63
 
61
- def all_background_emission_term_ids(cycle: dict):
62
- term_ids = cycle_emissions_in_system_boundary(cycle)
63
- return list(set([
64
- get_lookup_value({'termType': TermTermType.EMISSION.value, '@id': term_id}, 'inputProductionGroupId')
64
+ def _all_background_emission_term_ids(node: dict, termType: TermTermType):
65
+ term_ids = cycle_emissions_in_system_boundary(node, termType=termType)
66
+ background_ids = list(set([
67
+ get_lookup_value({'termType': termType.value, '@id': term_id}, 'inputProductionGroupId')
65
68
  for term_id in term_ids
66
69
  ]))
70
+ # make sure input production emission is itself in the system boundary
71
+ return [term_id for term_id in background_ids if term_id in term_ids]
67
72
 
68
73
 
69
- def log_missing_emissions(cycle: dict, **log_args):
70
- all_emission_term_ids = all_background_emission_term_ids(cycle)
74
+ def log_missing_emissions(node: dict, termType: TermTermType = TermTermType.EMISSION, **log_args):
75
+ all_emission_term_ids = _all_background_emission_term_ids(node, termType)
71
76
 
72
77
  def log_input(input_term_id: str, included_emission_term_ids: list, **extra_log_args):
73
- missing_emission_term_ids = [
78
+ missing_emission_term_ids = non_empty_list([
74
79
  term_id for term_id in all_emission_term_ids if term_id not in included_emission_term_ids
75
- ]
80
+ ])
81
+
76
82
  for emission_id in missing_emission_term_ids:
77
- logShouldRun(cycle, term=input_term_id, should_run=False, emission_id=emission_id,
83
+ # debug value on the emission itself so it appears for the input
84
+ debugValues(node, term=emission_id,
85
+ value=None,
86
+ coefficient=None,
87
+ input=input_term_id,
88
+ **log_args,
89
+ **extra_log_args)
90
+ logRequirements(node, term=input_term_id,
91
+ emission_id=emission_id,
92
+ has_emission_factor=False,
93
+ **log_args,
94
+ **extra_log_args)
95
+ logShouldRun(node, term=input_term_id, should_run=False,
96
+ emission_id=emission_id,
78
97
  **log_args,
79
98
  **extra_log_args)
80
99
  return log_input
100
+
101
+
102
+ _KEY_TO_FIELD = {
103
+ 'inputs': 'key'
104
+ }
105
+
106
+
107
+ def _key_to_field(key: str): return _KEY_TO_FIELD.get(key) or key
108
+
109
+
110
+ def _values_from_column(index_column: str, column: str, value: str):
111
+ values = column.split('+')
112
+ term_id = values[0]
113
+ value = safe_parse_float(value, default=None)
114
+ return {
115
+ term_id: {
116
+ 'value': value
117
+ } | {
118
+ _key_to_field(v.split('[')[0]): v.split('[')[1][:-1] for v in values[1:]
119
+ }
120
+ } if all([
121
+ column != index_column,
122
+ not column.startswith('ecoinvent'),
123
+ not column.startswith('ecoalim'),
124
+ not _is_missing_value(value)
125
+ ]) else {}
126
+
127
+
128
+ def convert_background_lookup(lookup, index_column: str):
129
+ columns = lookup_columns(lookup)
130
+ return {
131
+ row[index_column]: reduce(
132
+ lambda prev, curr: prev | _values_from_column(index_column, curr, row[curr]),
133
+ columns,
134
+ {}
135
+ )
136
+ for row in lookup
137
+ }
138
+
139
+
140
+ def parse_term_id(term_id: str): return term_id.split('-')[0]
141
+
142
+
143
+ def join_term_id(term_id: str, data: dict):
144
+ return '-'.join(non_empty_list([term_id] + list(_omit(data, ['value']).values())))
145
+
146
+
147
+ def _process_mapping(
148
+ node: dict,
149
+ input: dict,
150
+ term_type: TermTermType,
151
+ extract_mapping: Callable[[Tuple, TermTermType], Tuple[dict, float]],
152
+ **log_args
153
+ ) -> dict:
154
+ input_term_id = input.get('term', {}).get('@id')
155
+ operation_term_id = input.get('operation', {}).get('@id')
156
+ animal_term_id = input.get('animal', {}).get('@id')
157
+
158
+ def add(prev: dict, mapping: Tuple):
159
+ values, coefficient = extract_mapping(mapping, term_type)
160
+ for term_id, data in values:
161
+ # log run on each node so we know it did run
162
+ logShouldRun(node, term=input_term_id, should_run=True,
163
+ methodTier=EmissionMethodTier.BACKGROUND.value,
164
+ emission_id=term_id,
165
+ **log_args)
166
+ debugValues(node, term=term_id,
167
+ value=data.get('value'),
168
+ coefficient=coefficient,
169
+ input=input_term_id,
170
+ operation=operation_term_id,
171
+ animal=animal_term_id,
172
+ **log_args)
173
+ group_id = join_term_id(term_id, data)
174
+ prev[group_id] = prev.get(group_id, []) + [data | {'coefficient': coefficient}]
175
+ return prev
176
+ return add
177
+
178
+
179
+ def process_input_mappings(
180
+ node: dict,
181
+ input: dict,
182
+ mappings: list,
183
+ term_type: TermTermType,
184
+ extract_mapping: Callable[[tuple, TermTermType], Tuple[dict, float]],
185
+ **log_args
186
+ ):
187
+ return reduce(_process_mapping(node, input, term_type, extract_mapping, **log_args), mappings, {})
@@ -111,17 +111,6 @@ def properties_logs(blank_nodes: list, properties: Union[dict, list]):
111
111
  return log_as_table(logs)
112
112
 
113
113
 
114
- def group_by_keys(group_keys: list = ['term']):
115
- def run(group: dict, node: dict):
116
- group_key = '-'.join(non_empty_list([
117
- node.get(v, {}).get('@id') if isinstance(node.get(v), dict) else node.get(v)
118
- for v in group_keys
119
- ]))
120
- group[group_key] = group.get(group_key, []) + [node]
121
- return group
122
- return run
123
-
124
-
125
114
  def _module_term_id(term_id: str, module):
126
115
  term_id_str = term_id.split('.')[-1] if '.' in term_id else term_id
127
116
  return getattr(module, 'TERM_ID', term_id_str).split(',')[0]
@@ -1337,6 +1326,7 @@ def get_inputs_from_properties(input: dict, term_types: Union[TermTermType, List
1337
1326
  {
1338
1327
  'term': p.get('key'),
1339
1328
  'value': [(p.get('value') / 100) * (p.get('share', 100) / 100) * input_value],
1329
+ # for grouping
1340
1330
  'parent': term
1341
1331
  } for p in (properties or []) if all([p.get('key'), p.get('value')])
1342
1332
  ]) if input_value > 0 else []
@@ -2,7 +2,7 @@ from collections.abc import Iterable
2
2
  from typing import Optional, Union
3
3
  from hestia_earth.schema import EmissionMethodTier, SchemaType, TermTermType
4
4
  from hestia_earth.utils.model import linked_node
5
-
5
+ from hestia_earth.utils.emission import cycle_emissions_in_system_boundary, emissions_in_system_boundary
6
6
 
7
7
  from . import flatten_args
8
8
  from .term import download_term
@@ -13,20 +13,22 @@ from .constant import Units, get_atomic_conversion
13
13
  EMISSION_METHOD_TIERS = [e.value for e in EmissionMethodTier]
14
14
 
15
15
 
16
- def _new_emission(term, model=None):
16
+ def _new_emission(term, model=None, country_id: str = None, key_id: str = None):
17
17
  node = {'@type': SchemaType.EMISSION.value}
18
18
  node['term'] = linked_node(term if isinstance(term, dict) else download_term(term, TermTermType.EMISSION))
19
+ if country_id:
20
+ node['country'] = linked_node(download_term(country_id, TermTermType.REGION))
21
+ if key_id:
22
+ node['key'] = linked_node(download_term(key_id))
19
23
  return include_methodModel(node, model)
20
24
 
21
25
 
22
- def get_nh3_no3_nox_to_n(cycle: dict, nh3_term_id: str, no3_term_id: str, nox_term_id: str, allow_none: bool = False):
23
- default_value = 0 if allow_none else None
24
-
25
- nh3 = find_terms_value(cycle.get('emissions', []), nh3_term_id, default=default_value)
26
+ def get_nh3_no3_nox_to_n(cycle: dict, nh3_term_id: str = None, no3_term_id: str = None, nox_term_id: str = None):
27
+ nh3 = find_terms_value(cycle.get('emissions', []), nh3_term_id, default=None)
26
28
  nh3 = None if nh3 is None else nh3 / get_atomic_conversion(Units.KG_NH3, Units.TO_N)
27
- no3 = find_terms_value(cycle.get('emissions', []), no3_term_id, default=default_value)
29
+ no3 = find_terms_value(cycle.get('emissions', []), no3_term_id, default=None)
28
30
  no3 = None if no3 is None else no3 / get_atomic_conversion(Units.KG_NO3, Units.TO_N)
29
- nox = find_terms_value(cycle.get('emissions', []), nox_term_id, default=default_value)
31
+ nox = find_terms_value(cycle.get('emissions', []), nox_term_id, default=None)
30
32
  nox = None if nox is None else nox / get_atomic_conversion(Units.KG_NOX, Units.TO_N)
31
33
 
32
34
  return (nh3, no3, nox)
@@ -100,3 +102,11 @@ def to_emission_method_tier(method: Union[EmissionMethodTier, str]) -> Optional[
100
102
  def filter_emission_inputs(emission: dict, term_type: TermTermType):
101
103
  inputs = emission.get('inputs', [])
102
104
  return [i for i in inputs if i.get('termType') == term_type.value]
105
+
106
+
107
+ def background_emissions_in_system_boundary(node: dict, term_type: TermTermType = TermTermType.EMISSION):
108
+ term_ids = (
109
+ cycle_emissions_in_system_boundary(node, term_type) if term_type == TermTermType.EMISSION else
110
+ emissions_in_system_boundary(term_type)
111
+ )
112
+ return [id for id in term_ids if 'InputsProduction' in id]
@@ -47,7 +47,7 @@ def _map_properties(lookup, term_id: str, column_prefix: str):
47
47
  return {'value': value, 'sd': sd, 'min': min, 'max': max}
48
48
 
49
49
 
50
- def rescale_properties_from_dryMatter(model: str, node: dict, blank_nodes: list):
50
+ def rescale_properties_from_dryMatter(model: str, node: dict, blank_nodes: list, **log_args):
51
51
  properties = get_feedipedia_properties()
52
52
  # download all to save time
53
53
  term_types = [blank_node.get('term', {}).get('termType') for blank_node in blank_nodes]
@@ -78,7 +78,7 @@ def rescale_properties_from_dryMatter(model: str, node: dict, blank_nodes: list)
78
78
  ])
79
79
  ])
80
80
  for prop in new_properties:
81
- logShouldRun(node, model, term_id, True, property=prop.get('term', {}).get('@id'))
81
+ logShouldRun(node, model, term_id, True, property=prop.get('term', {}).get('@id'), **log_args)
82
82
  return (
83
83
  blank_node | {'properties': merge_blank_nodes(all_properties, new_properties)}
84
84
  ) if new_properties else blank_node
@@ -4,7 +4,7 @@ from hestia_earth.utils.model import filter_list_term_type
4
4
  from hestia_earth.utils.tools import list_sum, safe_parse_date
5
5
 
6
6
  from hestia_earth.models.log import debugValues
7
- from .lookup import all_factor_value, _region_factor_value, _aware_factor_value, fallback_country
7
+ from .lookup import all_factor_value, region_factor_value, aware_factor_value, fallback_country
8
8
  from .product import find_by_product
9
9
  from .site import region_level_1_id
10
10
 
@@ -190,7 +190,7 @@ def impact_country_value(
190
190
  blank_nodes=blank_nodes,
191
191
  grouped_key=group_key,
192
192
  default_no_values=default_no_values,
193
- factor_value_func=_region_factor_value
193
+ factor_value_func=region_factor_value
194
194
  )
195
195
 
196
196
 
@@ -219,10 +219,8 @@ def impact_aware_value(model: str, term_id: str, impact: dict, lookup: str, grou
219
219
  blank_nodes = impact.get('emissionsResourceUse', [])
220
220
  site = get_site(impact)
221
221
  aware_id = site.get('awareWaterBasinId')
222
- if aware_id is None:
223
- return None
224
222
 
225
- return all_factor_value(
223
+ return None if aware_id is None else all_factor_value(
226
224
  logs_model=model,
227
225
  logs_term_id=term_id,
228
226
  node=impact,
@@ -231,7 +229,7 @@ def impact_aware_value(model: str, term_id: str, impact: dict, lookup: str, grou
231
229
  blank_nodes=blank_nodes,
232
230
  grouped_key=group_key,
233
231
  default_no_values=None,
234
- factor_value_func=_aware_factor_value
232
+ factor_value_func=aware_factor_value
235
233
  )
236
234
 
237
235
 
@@ -5,7 +5,10 @@ from .method import include_methodModel
5
5
  from .term import download_term
6
6
 
7
7
 
8
- def _new_indicator(term, model=None, land_cover_id: str = None, previous_land_cover_id: str = None):
8
+ def _new_indicator(
9
+ term: dict, model=None,
10
+ land_cover_id: str = None, previous_land_cover_id: str = None, country_id: str = None, key_id: str = None
11
+ ):
9
12
  node = {'@type': SchemaType.INDICATOR.value}
10
13
  node['term'] = linked_node(term if isinstance(term, dict) else download_term(
11
14
  term, TermTermType.CHARACTERISEDINDICATOR)
@@ -14,4 +17,8 @@ def _new_indicator(term, model=None, land_cover_id: str = None, previous_land_co
14
17
  node['landCover'] = linked_node(download_term(land_cover_id, TermTermType.LANDCOVER))
15
18
  if previous_land_cover_id:
16
19
  node['previousLandCover'] = linked_node(download_term(previous_land_cover_id, TermTermType.LANDCOVER))
20
+ if country_id:
21
+ node['country'] = linked_node(download_term(country_id, TermTermType.REGION))
22
+ if key_id:
23
+ node['key'] = linked_node(download_term(key_id))
17
24
  return include_methodModel(node, model)
@@ -19,16 +19,20 @@ def _node_value(node):
19
19
 
20
20
 
21
21
  def _factor_value(model: str, term_id: str, lookup_name: str, lookup_col: str, grouped_key: Optional[str] = None):
22
- def get_value(data: dict):
23
- node_term_id = data.get('term', {}).get('@id')
24
- grouped_data_key = grouped_key or data.get('methodModel', {}).get('@id')
25
- value = _node_value(data)
22
+ @lru_cache()
23
+ def get_coefficient(node_term_id: str, grouped_data_key: str):
26
24
  coefficient = get_region_lookup_value(lookup_name, node_term_id, lookup_col, model=model, term=term_id)
27
25
  # value is either a number or matching between a model and a value (restrict value to specific model only)
28
- coefficient = safe_parse_float(
26
+ return safe_parse_float(
29
27
  extract_grouped_data(coefficient, grouped_data_key),
30
28
  default=None
31
29
  ) if ':' in str(coefficient) else safe_parse_float(coefficient, default=None)
30
+
31
+ def get_value(data: dict):
32
+ node_term_id = data.get('term', {}).get('@id')
33
+ grouped_data_key = grouped_key or data.get('methodModel', {}).get('@id')
34
+ value = _node_value(data)
35
+ coefficient = get_coefficient(node_term_id, grouped_data_key)
32
36
  if value is not None and coefficient is not None:
33
37
  if model:
34
38
  debugValues(data, model=model, term=term_id,
@@ -40,7 +44,15 @@ def _factor_value(model: str, term_id: str, lookup_name: str, lookup_col: str, g
40
44
  return get_value
41
45
 
42
46
 
43
- def _region_factor_value(model: str, term_id: str, lookup_name: str, lookup_term_id: str, group_key: str = None):
47
+ def region_factor_value(model: str, term_id: str, lookup_name: str, lookup_term_id: str, group_key: str = None):
48
+ @lru_cache()
49
+ def get_coefficient(node_term_id: str, region_term_id: str):
50
+ coefficient = get_region_lookup_value(lookup_name, region_term_id, node_term_id, model=model, term=term_id)
51
+ return safe_parse_float(
52
+ extract_grouped_data(coefficient, group_key) if group_key else coefficient,
53
+ default=None
54
+ )
55
+
44
56
  def get_value(data: dict):
45
57
  node_term_id = data.get('term', {}).get('@id')
46
58
  value = _node_value(data)
@@ -48,11 +60,7 @@ def _region_factor_value(model: str, term_id: str, lookup_name: str, lookup_term
48
60
  region_term_id = (
49
61
  (data.get('region') or data.get('country') or {'@id': lookup_term_id}).get('@id')
50
62
  ) if lookup_term_id.startswith('GADM-') else lookup_term_id
51
- coefficient = get_region_lookup_value(lookup_name, region_term_id, node_term_id, model=model, term=term_id)
52
- coefficient = safe_parse_float(
53
- extract_grouped_data(coefficient, group_key) if group_key else coefficient,
54
- default=None
55
- )
63
+ coefficient = get_coefficient(node_term_id, region_term_id)
56
64
  if value is not None and coefficient is not None:
57
65
  debugValues(data, model=model, term=term_id,
58
66
  node=node_term_id,
@@ -62,26 +70,30 @@ def _region_factor_value(model: str, term_id: str, lookup_name: str, lookup_term
62
70
  return get_value
63
71
 
64
72
 
65
- def _aware_factor_value(model: str, term_id: str, lookup_name: str, aware_id: str, group_key: str = None):
73
+ def aware_factor_value(model: str, term_id: str, lookup_name: str, aware_id: str, group_key: str = None):
66
74
  lookup = download_lookup(lookup_name, False) # avoid saving in memory as there could be many different files used
67
75
  lookup_col = column_name('awareWaterBasinId')
68
76
 
77
+ @lru_cache()
78
+ def get_coefficient(node_term_id: str):
79
+ coefficient = _get_single_table_value(lookup, lookup_col, int(aware_id), column_name(node_term_id))
80
+ return safe_parse_float(
81
+ extract_grouped_data(coefficient, group_key),
82
+ default=None
83
+ ) if group_key else coefficient
84
+
69
85
  def get_value(data: dict):
70
86
  node_term_id = data.get('term', {}).get('@id')
71
87
  value = _node_value(data)
72
88
 
73
89
  try:
74
- coefficient = _get_single_table_value(lookup, lookup_col, int(aware_id), column_name(node_term_id))
75
- coefficient = safe_parse_float(
76
- extract_grouped_data(coefficient, group_key),
77
- default=None
78
- ) if group_key else coefficient
90
+ coefficient = get_coefficient(node_term_id)
79
91
  if value is not None and coefficient is not None:
80
92
  debugValues(data, model=model, term=term_id,
81
93
  node=node_term_id,
82
94
  value=value,
83
95
  coefficient=coefficient)
84
- except ValueError: # factor does not exist
96
+ except Exception: # factor does not exist
85
97
  coefficient = None
86
98
 
87
99
  return {'id': node_term_id, 'value': value, 'coefficient': coefficient}
@@ -17,5 +17,5 @@ PRODUCTIVITY_KEY = {
17
17
 
18
18
 
19
19
  def get_productivity(country: dict, default: PRODUCTIVITY = PRODUCTIVITY.HIGH):
20
- hdi = safe_parse_float(get_lookup_value(country, 'hdi'), default=None)
20
+ hdi = safe_parse_float(get_lookup_value(country, 'HDI'), default=None)
21
21
  return next((key for key in PRODUCTIVITY_KEY if hdi and PRODUCTIVITY_KEY[key](hdi)), default)
@@ -1 +1 @@
1
- VERSION = '0.73.7'
1
+ VERSION = '0.74.0'
@@ -3,6 +3,8 @@ import sys
3
3
  import logging
4
4
 
5
5
  LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
6
+ _EXTENDED_LOGS = os.getenv('LOG_EXTENDED', 'true') == 'true'
7
+ _LOG_DATE_FORMAT = os.getenv('LOG_DATE_FORMAT', '%Y-%m-%dT%H:%M:%S%z')
6
8
 
7
9
  # disable root logger
8
10
  root_logger = logging.getLogger()
@@ -25,9 +27,12 @@ def log_to_file(filepath: str):
25
27
  Path of the file.
26
28
  """
27
29
  formatter = logging.Formatter(
28
- '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", '
29
- '"filename": "%(filename)s", "message": "%(message)s"}',
30
- '%Y-%m-%dT%H:%M:%S%z')
30
+ '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "logger": "%(name)s", "message": "%(message)s"}',
31
+ _LOG_DATE_FORMAT
32
+ ) if _EXTENDED_LOGS else logging.Formatter(
33
+ '{"logger": "%(name)s", "message": "%(message)s"}',
34
+ _LOG_DATE_FORMAT
35
+ )
31
36
  handler = logging.FileHandler(filepath, encoding='utf-8')
32
37
  handler.setFormatter(formatter)
33
38
  handler.setLevel(logging.getLevelName(LOG_LEVEL))
@@ -1,7 +1,7 @@
1
1
  import pydash
2
2
  from datetime import datetime
3
3
  from hestia_earth.schema import UNIQUENESS_FIELDS
4
- from hestia_earth.utils.tools import safe_parse_date
4
+ from hestia_earth.utils.tools import safe_parse_date, flatten
5
5
 
6
6
  from hestia_earth.orchestrator.utils import _non_empty_list, update_node_version
7
7
  from .merge_node import merge as merge_node
@@ -27,39 +27,6 @@ def _has_property(value: dict, key: str):
27
27
  def _values_have_property(values: list, key: str): return any([_has_property(v, key) for v in values])
28
28
 
29
29
 
30
- def _match_list_el(source: list, dest: list, key: str):
31
- src_value = sorted(_non_empty_list([pydash.objects.get(x, key) for x in source]))
32
- dest_value = sorted(_non_empty_list([pydash.objects.get(x, key) for x in dest]))
33
- return src_value == dest_value
34
-
35
-
36
- def _get_value(data: dict, key: str, merge_args: dict = {}):
37
- value = pydash.objects.get(data, key)
38
- date = safe_parse_date(value) if key in ['startDate', 'endDate'] else None
39
- return datetime.strftime(date, merge_args.get('matchDatesFormat', '%Y-%m-%d')) if date else value
40
-
41
-
42
- def _match_el(source: dict, dest: dict, keys: list, merge_args: dict = {}):
43
- def match(key: str):
44
- keys = key.split('.')
45
- src_value = _get_value(source, key, merge_args)
46
- dest_value = _get_value(dest, key, merge_args)
47
- is_list = len(keys) >= 2 and (
48
- isinstance(pydash.objects.get(source, keys[0]), list) or
49
- isinstance(pydash.objects.get(dest, keys[0]), list)
50
- )
51
- return _match_list_el(
52
- pydash.objects.get(source, keys[0], []),
53
- pydash.objects.get(dest, keys[0], []),
54
- '.'.join(keys[1:])
55
- ) if is_list else src_value == dest_value
56
-
57
- source_properties = [p for p in keys if _has_property(source, p)]
58
- dest_properties = [p for p in keys if _has_property(dest, p)]
59
-
60
- return all(map(match, source_properties)) if source_properties == dest_properties else False
61
-
62
-
63
30
  def _handle_local_property(values: list, properties: list, local_id: str):
64
31
  # Handle "impactAssessment.@id" if present in the data
65
32
  existing_id = local_id.replace('.id', '.@id')
@@ -76,38 +43,58 @@ def _handle_local_property(values: list, properties: list, local_id: str):
76
43
  return properties
77
44
 
78
45
 
79
- def _find_match_el_index(values: list, el: dict, same_methodModel: bool, model: dict, node_type: str, merge_args: dict):
80
- """
81
- Find an element in the values that match the new element, based on the unique properties.
82
- To find a matching element:
46
+ def _get_value(data: dict, key: str, merge_args: dict = {}):
47
+ value = pydash.objects.get(data, key)
48
+ date = safe_parse_date(value) if key in ['startDate', 'endDate'] else None
49
+ return datetime.strftime(date, merge_args.get('matchDatesFormat', '%Y-%m-%d')) if date else value
50
+
51
+
52
+ def _value_index_key(value: dict, properties: list, merge_args: dict = {}):
53
+ def property_value(key: str):
54
+ keys = key.split('.')
55
+ prop_value = _get_value(value, key, merge_args)
56
+ is_list = len(keys) >= 2 and isinstance(pydash.objects.get(value, keys[0]), list)
57
+ return sorted(_non_empty_list([
58
+ pydash.objects.get(x, '.'.join(keys[1:]))
59
+ for x in pydash.objects.get(value, keys[0], [])
60
+ ])) if is_list else prop_value
61
+
62
+ source_properties = [p for p in properties if _has_property(value, p)]
63
+ return '-'.join(map(str, flatten(map(property_value, source_properties))))
64
+
65
+
66
+ def _build_matching_properties(values: list, model: dict = {}, merge_args: dict = {}, node_type: str = ''):
67
+ # only merge node if it has the same `methodModel`
68
+ same_methodModel = merge_args.get('sameMethodModel', False)
83
69
 
84
- 1. Update list of properties to handle `methodModel.@id` and `impactAssessment.@id`
85
- 2. Filter values that have the same unique properties as el
86
- 3. Make sure all shared unique properties are identical
87
- """
88
70
  properties = _matching_properties(model, node_type)
89
71
  properties = list(set(properties + [_METHOD_MODEL_KEY])) if same_methodModel else [
90
72
  p for p in properties if p != _METHOD_MODEL_KEY
91
73
  ]
92
- properties = _handle_local_property(values, properties, 'impactAssessment.id')
74
+ return _handle_local_property(values, properties, 'impactAssessment.id')
93
75
 
94
- return next(
95
- (i for i in range(len(values)) if _match_el(values[i], el, properties, merge_args)),
96
- None
97
- ) if properties else None
98
76
 
77
+ def merge(source: list, new_values: list, version: str, model: dict = {}, merge_args: dict = {}, node_type: str = ''):
78
+ source = [] if source is None else source
99
79
 
100
- def merge(source: list, merge_with: list, version: str, model: dict = {}, merge_args: dict = {}, node_type: str = ''):
101
- source = source if source is not None else []
102
-
103
- # only merge node if it has the same `methodModel`
104
- same_methodModel = merge_args.get('sameMethodModel', False)
105
80
  # only merge if the
106
81
  skip_same_term = merge_args.get('skipSameTerm', False)
107
82
 
108
- for el in _non_empty_list(merge_with):
109
- source_index = _find_match_el_index(source, el, same_methodModel, model, node_type, merge_args)
83
+ # build list of properties used to do the matching
84
+ properties = _build_matching_properties(source, model, merge_args, node_type)
85
+
86
+ source_index_keys = {
87
+ _value_index_key(value, properties, merge_args): index
88
+ for index, value in enumerate(source)
89
+ } if properties else None
90
+
91
+ for el in _non_empty_list(new_values):
92
+ new_value_index_key = _value_index_key(el, properties, merge_args)
93
+ source_index = source_index_keys.get(new_value_index_key) if source_index_keys else None
110
94
  if source_index is None:
95
+ # add to index keys for next elements
96
+ if source_index_keys:
97
+ source_index_keys[new_value_index_key] = len(source)
111
98
  source.append(update_node_version(version, el))
112
99
  elif not skip_same_term:
113
100
  source[source_index] = merge_node(source[source_index], el, version, model, merge_args)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hestia-earth-models
3
- Version: 0.73.7
3
+ Version: 0.74.0
4
4
  Summary: HESTIA's set of modules for filling gaps in the activity data using external datasets (e.g. populating soil properties with a geospatial dataset using provided coordinates) and internal lookups (e.g. populating machinery use from fuel use). Includes rules for when gaps should be filled versus not (e.g. never gap fill yield, gap fill crop residue if yield provided etc.).
5
5
  Home-page: https://gitlab.com/hestia-earth/hestia-engine-models
6
6
  Author: HESTIA Team
@@ -11,8 +11,8 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
11
11
  Classifier: Programming Language :: Python :: 3.6
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: hestia-earth-schema==33.*
15
- Requires-Dist: hestia-earth-utils>=0.14.9
14
+ Requires-Dist: hestia-earth-schema<34.0.0,>=33.5.0
15
+ Requires-Dist: hestia-earth-utils>=0.15.1
16
16
  Requires-Dist: python-dateutil>=2.8.1
17
17
  Requires-Dist: CurrencyConverter==0.16.8
18
18
  Requires-Dist: haversine>=2.7.0