hestia-earth-models 0.73.4__py3-none-any.whl → 0.73.6__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (24) hide show
  1. hestia_earth/models/config/Cycle.json +0 -45
  2. hestia_earth/models/config/ImpactAssessment.json +0 -24
  3. hestia_earth/models/config/Site.json +11 -0
  4. hestia_earth/models/geospatialDatabase/potentialEvapotranspirationAnnual.py +2 -1
  5. hestia_earth/models/geospatialDatabase/precipitationAnnual.py +2 -1
  6. hestia_earth/models/geospatialDatabase/temperatureAnnual.py +2 -1
  7. hestia_earth/models/hestia/landCover.py +105 -36
  8. hestia_earth/models/ipcc2019/biocharOrganicCarbonPerHa.py +435 -0
  9. hestia_earth/models/mocking/search-results.json +706 -702
  10. hestia_earth/models/utils/__init__.py +10 -1
  11. hestia_earth/models/utils/blank_node.py +1 -1
  12. hestia_earth/models/version.py +1 -1
  13. {hestia_earth_models-0.73.4.dist-info → hestia_earth_models-0.73.6.dist-info}/METADATA +2 -2
  14. {hestia_earth_models-0.73.4.dist-info → hestia_earth_models-0.73.6.dist-info}/RECORD +20 -21
  15. tests/models/hestia/test_landCover.py +58 -2
  16. tests/models/ipcc2019/test_biocharOrganicCarbonPerHa.py +131 -0
  17. tests/models/test_utils.py +6 -0
  18. hestia_earth/models/faostat2018/landTransformation100YearAverageDuringCycle.py +0 -34
  19. hestia_earth/models/faostat2018/landTransformation20YearAverageDuringCycle.py +0 -34
  20. tests/models/faostat2018/test_landTransformation100YearAverageDuringCycle.py +0 -21
  21. tests/models/faostat2018/test_landTransformation20YearAverageDuringCycle.py +0 -21
  22. {hestia_earth_models-0.73.4.dist-info → hestia_earth_models-0.73.6.dist-info}/LICENSE +0 -0
  23. {hestia_earth_models-0.73.4.dist-info → hestia_earth_models-0.73.6.dist-info}/WHEEL +0 -0
  24. {hestia_earth_models-0.73.4.dist-info → hestia_earth_models-0.73.6.dist-info}/top_level.txt +0 -0
@@ -2093,51 +2093,6 @@
2093
2093
  },
2094
2094
  "stage": 2
2095
2095
  },
2096
- {
2097
- "key": "emissions",
2098
- "model": "ipcc2006",
2099
- "value": "n2OToAirCropResidueDecompositionIndirect",
2100
- "runStrategy": "add_blank_node_if_missing",
2101
- "runArgs": {
2102
- "runNonMeasured": true,
2103
- "runNonAddedTerm": true
2104
- },
2105
- "mergeStrategy": "list",
2106
- "mergeArgs": {
2107
- "replaceThreshold": ["value", 0.01]
2108
- },
2109
- "stage": 2
2110
- },
2111
- {
2112
- "key": "emissions",
2113
- "model": "ipcc2006",
2114
- "value": "n2OToAirCropResidueDecompositionDirect",
2115
- "runStrategy": "add_blank_node_if_missing",
2116
- "runArgs": {
2117
- "runNonMeasured": true,
2118
- "runNonAddedTerm": true
2119
- },
2120
- "mergeStrategy": "list",
2121
- "mergeArgs": {
2122
- "replaceThreshold": ["value", 0.01]
2123
- },
2124
- "stage": 2
2125
- },
2126
- {
2127
- "key": "emissions",
2128
- "model": "ipcc2006",
2129
- "value": "n2OToAirExcretaIndirect",
2130
- "runStrategy": "add_blank_node_if_missing",
2131
- "runArgs": {
2132
- "runNonMeasured": true,
2133
- "runNonAddedTerm": true
2134
- },
2135
- "mergeStrategy": "list",
2136
- "mergeArgs": {
2137
- "replaceThreshold": ["value", 0.01]
2138
- },
2139
- "stage": 2
2140
- },
2141
2096
  {
2142
2097
  "key": "emissions",
2143
2098
  "model": "jarvisAndPain1994",
@@ -203,30 +203,6 @@
203
203
  "stage": 1
204
204
  }
205
205
  ],
206
- [
207
- {
208
- "key": "emissionsResourceUse",
209
- "model": "faostat2018",
210
- "value": "landTransformation20YearAverageDuringCycle",
211
- "runStrategy": "always",
212
- "mergeStrategy": "list",
213
- "mergeArgs": {
214
- "replaceThreshold": ["value", 0.01]
215
- },
216
- "stage": 1
217
- },
218
- {
219
- "key": "emissionsResourceUse",
220
- "model": "faostat2018",
221
- "value": "landTransformation100YearAverageDuringCycle",
222
- "runStrategy": "always",
223
- "mergeStrategy": "list",
224
- "mergeArgs": {
225
- "replaceThreshold": ["value", 0.01]
226
- },
227
- "stage": 1
228
- }
229
- ],
230
206
  {
231
207
  "key": "emissionsResourceUse",
232
208
  "model": "resourceUseNotRelevant",
@@ -523,6 +523,17 @@
523
523
  "replaceThreshold": ["value", 0.01]
524
524
  },
525
525
  "stage": 2
526
+ },
527
+ {
528
+ "key": "measurements",
529
+ "model": "ipcc2019",
530
+ "value": "biocharOrganicCarbonPerHa",
531
+ "runStrategy": "always",
532
+ "mergeStrategy": "list",
533
+ "mergeArgs": {
534
+ "replaceThreshold": ["value", 0.01]
535
+ },
536
+ "stage": 2
526
537
  }
527
538
  ]
528
539
  ]
@@ -2,6 +2,7 @@ from hestia_earth.schema import MeasurementMethodClassification
2
2
  from hestia_earth.utils.tools import non_empty_list
3
3
 
4
4
  from hestia_earth.models.log import logRequirements, logShouldRun
5
+ from hestia_earth.models.utils import max_date
5
6
  from hestia_earth.models.utils.measurement import _new_measurement
6
7
  from hestia_earth.models.utils.site import related_years
7
8
  from .utils import download, has_geospatial_data, should_download
@@ -39,7 +40,7 @@ def _measurement(value: float, year: int):
39
40
  measurement['value'] = [value]
40
41
  measurement['methodClassification'] = MeasurementMethodClassification.GEOSPATIAL_DATASET.value
41
42
  measurement['startDate'] = f"{year}-01-01"
42
- measurement['endDate'] = f"{year}-12-31"
43
+ measurement['endDate'] = max_date(f"{year}-12-31")
43
44
  return measurement
44
45
 
45
46
 
@@ -2,6 +2,7 @@ from hestia_earth.schema import MeasurementMethodClassification
2
2
  from hestia_earth.utils.tools import non_empty_list
3
3
 
4
4
  from hestia_earth.models.log import logRequirements, logShouldRun
5
+ from hestia_earth.models.utils import max_date
5
6
  from hestia_earth.models.utils.measurement import _new_measurement
6
7
  from hestia_earth.models.utils.source import get_source
7
8
  from hestia_earth.models.utils.site import related_years
@@ -41,7 +42,7 @@ def _measurement(site: dict, value: float, year: int):
41
42
  measurement['value'] = [value]
42
43
  measurement['methodClassification'] = MeasurementMethodClassification.GEOSPATIAL_DATASET.value
43
44
  measurement['startDate'] = f"{year}-01-01"
44
- measurement['endDate'] = f"{year}-12-31"
45
+ measurement['endDate'] = max_date(f"{year}-12-31")
45
46
  return measurement | get_source(site, BIBLIO_TITLE)
46
47
 
47
48
 
@@ -2,6 +2,7 @@ from hestia_earth.schema import MeasurementMethodClassification
2
2
  from hestia_earth.utils.tools import non_empty_list
3
3
 
4
4
  from hestia_earth.models.log import logRequirements, logShouldRun
5
+ from hestia_earth.models.utils import max_date
5
6
  from hestia_earth.models.utils.measurement import _new_measurement
6
7
  from hestia_earth.models.utils.source import get_source
7
8
  from hestia_earth.models.utils.site import related_years
@@ -40,7 +41,7 @@ def _measurement(site: dict, value: float, year: int):
40
41
  measurement = _new_measurement(TERM_ID)
41
42
  measurement['value'] = [value]
42
43
  measurement['startDate'] = f"{year}-01-01"
43
- measurement['endDate'] = f"{year}-12-31"
44
+ measurement['endDate'] = max_date(f"{year}-12-31")
44
45
  measurement['methodClassification'] = MeasurementMethodClassification.GEOSPATIAL_DATASET.value
45
46
  return measurement | get_source(site, BIBLIO_TITLE)
46
47
 
@@ -2,7 +2,6 @@ import functools
2
2
  import math
3
3
  from collections import defaultdict
4
4
  from datetime import datetime, timedelta
5
-
6
5
  from hestia_earth.schema import SiteSiteType, TermTermType
7
6
  from hestia_earth.utils.lookup import (
8
7
  download_lookup, get_table_value, column_name, _is_missing_value, extract_grouped_data, lookup_columns
@@ -64,6 +63,11 @@ RETURNS = {
64
63
  }]
65
64
  }
66
65
  LOOKUPS = {
66
+ "region-crop-cropGroupingFAOSTAT-landCover-annualCropland": "",
67
+ "region-crop-cropGroupingFAOSTAT-landCover-forest": "",
68
+ "region-crop-cropGroupingFAOSTAT-landCover-otherLand": "",
69
+ "region-crop-cropGroupingFAOSTAT-landCover-permanentCropland": "",
70
+ "region-crop-cropGroupingFAOSTAT-landCover-permanentPasture": "",
67
71
  "region-crop-cropGroupingFaostatProduction-areaHarvestedUpTo20YearExpansion": "",
68
72
  "region-crop-cropGroupingFaostatProduction-areaHarvested": "",
69
73
  "region-faostatArea-UpTo20YearExpansion": "",
@@ -82,18 +86,30 @@ LOOKUPS = {
82
86
  }
83
87
  MODEL_KEY = 'landCover'
84
88
 
85
- LAND_AREA = LOOKUPS["region-faostatArea"][3]
86
- SITE_TYPES = {
89
+ _LAND_AREA = LOOKUPS["region-faostatArea"][3]
90
+ _ALLOWED_SITE_TYPES = {
87
91
  SiteSiteType.CROPLAND.value,
88
92
  SiteSiteType.FOREST.value,
89
93
  SiteSiteType.OTHER_NATURAL_VEGETATION.value,
90
94
  SiteSiteType.PERMANENT_PASTURE.value
91
95
  }
92
- DEFAULT_WINDOW_IN_YEARS = 20
93
- DATE_TOLERANCE_IN_YEARS = 2
94
- OUTPUT_SIGNIFICANT_DIGITS = 3
95
- ALLOWED_LAND_USE_TYPES = [ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE]
96
+ _BUILDING_SITE_TYPES = [
97
+ SiteSiteType.AGRI_FOOD_PROCESSOR.value,
98
+ SiteSiteType.ANIMAL_HOUSING.value,
99
+ SiteSiteType.FOOD_RETAILER.value
100
+ ]
101
+ _DEFAULT_WINDOW_IN_YEARS = 20
102
+ _DATE_TOLERANCE_IN_YEARS = 2
103
+ _OUTPUT_SIGNIFICANT_DIGITS = 3
104
+ _ALLOWED_LAND_USE_TYPES = [ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE]
96
105
  _LOOKUP_EXPANSION = "region-crop-cropGroupingFaostatProduction-areaHarvestedUpTo20YearExpansion.csv"
106
+ _COMPLETE_CHANGES_OTHER_LAND = {
107
+ OTHER_LAND: 1,
108
+ FOREST_LAND: 0,
109
+ PERMANENT_PASTURE: 0,
110
+ ANNUAL_CROPLAND: 0,
111
+ PERMANENT_CROPLAND: 0
112
+ }
97
113
 
98
114
 
99
115
  def _get_lookup_with_cache(lookup_term, column):
@@ -116,6 +132,41 @@ def _get_lookup_with_cache(lookup_term, column):
116
132
  )
117
133
 
118
134
 
135
+ def _get_land_cover_lookup_suffix(land_type: str) -> str:
136
+ return LAND_USE_TERMS_FOR_TRANSFORMATION[land_type][0]
137
+
138
+
139
+ def get_landCover_lookups(country_id: str, end_year: int, product_name: str):
140
+ """
141
+ Attempts to get the pre-calculated values for the landCover model calculation.
142
+ Returns: {"Arable land": <value>, "Forest land": <value>, "Other land": <value>,
143
+ "Permanent crops": <value>, "Permanent meadows and pastures": <value>}
144
+ Missing values are returned as None.
145
+ """
146
+ lookup_prefix = 'region-crop-cropGroupingFAOSTAT-landCover'
147
+ return {
148
+ # Divide by 100 to match site_area ratios
149
+ land_type: value / 100 if value is not None else value
150
+ for land_type, value in
151
+ {
152
+ land_type: safe_parse_float(
153
+ value=extract_grouped_data(
154
+ data=get_region_lookup_value(
155
+ lookup_name=f"{lookup_prefix}-{_get_land_cover_lookup_suffix(land_type)}.csv",
156
+ term_id=country_id,
157
+ column=product_name,
158
+ model=MODEL,
159
+ key=MODEL_KEY
160
+ ),
161
+ key=str(end_year)
162
+ ),
163
+ default=None
164
+ )
165
+ for land_type in LAND_USE_TERMS_FOR_TRANSFORMATION.keys()
166
+ }.items()
167
+ }
168
+
169
+
119
170
  def _management(term_id: str, value: float, start_date: str, end_date: str):
120
171
  node = _new_management(term_id, MODEL)
121
172
  node['value'] = value
@@ -176,7 +227,7 @@ def _get_changes(country_id: str, end_year: int) -> tuple[dict, bool]:
176
227
  ),
177
228
  default=None
178
229
  )
179
- for land_use_term in ALL_LAND_USE_TERMS + [LAND_AREA]
230
+ for land_use_term in ALL_LAND_USE_TERMS + [_LAND_AREA]
180
231
  }
181
232
  missing_changes = [k for k, v in changes_dict.items() if v is None]
182
233
  changes_dict = {k: v if v is not None else 0 for k, v in changes_dict.items()}
@@ -267,7 +318,7 @@ def _allocate_pasture_loss_to_cropland(changes: dict, land_required_for_cropland
267
318
 
268
319
 
269
320
  def _allocate_other_land(
270
- changes: dict, max_forest_loss_to: dict, pasture_loss_to_cropland: float, cropland_loss_to_pasture: float
321
+ changes: dict, max_forest_loss_to: dict, pasture_loss_to_cropland: float, cropland_loss_to_pasture: float
271
322
  ) -> dict:
272
323
  """Allocate changes between Other land and cropland"""
273
324
  other_land_loss_to_cropland = (
@@ -461,7 +512,7 @@ def _get_term_id_for_crop(nodes: set, land_type: str) -> str:
461
512
 
462
513
 
463
514
  def _run(site: dict, existing_nodes: list, percentage_transformed_from: dict):
464
- start_year = _get_year_from_landCover(existing_nodes[0]) - DEFAULT_WINDOW_IN_YEARS
515
+ start_year = _get_year_from_landCover(existing_nodes[0]) - _DEFAULT_WINDOW_IN_YEARS
465
516
 
466
517
  """Creates a list of new management nodes, excluding any dates matching existing ones."""
467
518
  existing_nodes_set = {
@@ -476,7 +527,7 @@ def _run(site: dict, existing_nodes: list, percentage_transformed_from: dict):
476
527
  "land_type": land_type,
477
528
  "percentage": 0 if ratio == -0.0 else to_precision(
478
529
  number=ratio * 100,
479
- digits=OUTPUT_SIGNIFICANT_DIGITS
530
+ digits=_OUTPUT_SIGNIFICANT_DIGITS
480
531
  ),
481
532
  "term_id": _get_term_id_for_crop(existing_nodes_set, land_type=land_type)
482
533
  }
@@ -565,15 +616,17 @@ def _get_sums_of_crop_expansion(country_id: str, year: int, include_negatives: b
565
616
  def _get_net_expansion_cultivated_vs_harvested(
566
617
  annual_crops_net_expansion, changes, land_use_type, permanent_crops_net_expansion
567
618
  ):
568
- if land_use_type == ANNUAL_CROPLAND:
569
- net_expansion_cultivated_vs_harvested = _safe_divide(numerator=max(0, changes[ANNUAL_CROPLAND]),
570
- denominator=(annual_crops_net_expansion / 1000))
571
- elif land_use_type == PERMANENT_CROPLAND:
572
- net_expansion_cultivated_vs_harvested = _safe_divide(numerator=max(0, changes[PERMANENT_CROPLAND]),
573
- denominator=(permanent_crops_net_expansion / 1000))
574
- else:
575
- net_expansion_cultivated_vs_harvested = 1
576
- return net_expansion_cultivated_vs_harvested
619
+ return (
620
+ _safe_divide(
621
+ numerator=max(0, changes[ANNUAL_CROPLAND]),
622
+ denominator=(annual_crops_net_expansion / 1000)
623
+ ) if land_use_type == ANNUAL_CROPLAND else
624
+ _safe_divide(
625
+ numerator=max(0, changes[PERMANENT_CROPLAND]),
626
+ denominator=(permanent_crops_net_expansion / 1000)
627
+ ) if land_use_type == PERMANENT_CROPLAND else
628
+ 1
629
+ )
577
630
 
578
631
 
579
632
  def _get_year_from_landCover(node: dict):
@@ -739,7 +792,7 @@ def _should_run_historical_land_use_change_single_crop(
739
792
  capped_site_area = cap_values(dictionary=_scale_site_area_errors(site_area))
740
793
 
741
794
  sum_of_site_areas_is_100 = site_area_sum_to_100(capped_site_area)
742
- site_type_allowed = site.get("siteType") in SITE_TYPES
795
+ site_type_allowed = site.get("siteType") in _ALLOWED_SITE_TYPES
743
796
 
744
797
  logRequirements(
745
798
  log_node=site,
@@ -787,9 +840,9 @@ def _no_prior_land_cover_data(nodes: list, target_node: dict) -> bool:
787
840
  target_date = (
788
841
  datetime.strptime(target_node.get('startDate') or target_node.get('endDate'),
789
842
  DatestrFormat.YEAR_MONTH_DAY.value)
790
- - timedelta(days=DEFAULT_WINDOW_IN_YEARS * DAYS_IN_YEAR)
843
+ - timedelta(days=_DEFAULT_WINDOW_IN_YEARS * DAYS_IN_YEAR)
791
844
  )
792
- tolerance = timedelta(days=DATE_TOLERANCE_IN_YEARS * DAYS_IN_YEAR)
845
+ tolerance = timedelta(days=_DATE_TOLERANCE_IN_YEARS * DAYS_IN_YEAR)
793
846
  previous_nodes = [
794
847
  node for node in nodes
795
848
  if datetime.strptime(node.get("startDate"), DatestrFormat.YEAR_MONTH_DAY.value) - tolerance
@@ -800,16 +853,18 @@ def _no_prior_land_cover_data(nodes: list, target_node: dict) -> bool:
800
853
 
801
854
 
802
855
  def _should_run(site: dict) -> tuple[bool, list, dict]:
803
- management_nodes = _collect_land_use_types(
804
- [
805
- node for node in filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
806
- if not _should_group_landCover(node)
807
- ]
808
- )
856
+ is_site_building = site.get('siteType') in _BUILDING_SITE_TYPES
857
+
858
+ allowed_land_use_types = _ALLOWED_LAND_USE_TYPES + ([OTHER_LAND] if is_site_building else [])
859
+
860
+ management_nodes = _collect_land_use_types([
861
+ node for node in filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
862
+ if not _should_group_landCover(node)
863
+ ])
809
864
  relevant_nodes = sorted(
810
865
  [
811
866
  node for node in management_nodes
812
- if node["land-use-type"] in ALLOWED_LAND_USE_TYPES
867
+ if node["land-use-type"] in allowed_land_use_types
813
868
  ],
814
869
  key=lambda n: n.get("startDate") or n.get("endDate")
815
870
  )
@@ -821,18 +876,32 @@ def _should_run(site: dict) -> tuple[bool, list, dict]:
821
876
  target_node=relevant_nodes[0]
822
877
  ) if relevant_nodes else None
823
878
 
824
- should_run_nodes, site_area = _should_run_historical_land_use_change(
825
- site=site,
826
- nodes=relevant_nodes,
827
- land_use_type=land_use_type
828
- ) if all([land_use_type, has_no_prior_land_cover_data]) else (False, {})
879
+ landCover_from_lookups = get_landCover_lookups(
880
+ country_id=site.get("country", {}).get("@id"),
881
+ end_year=_get_year_from_landCover(relevant_nodes[0]),
882
+ product_name=relevant_nodes[0].get("term", {}).get("name", "")
883
+ ) if relevant_nodes else {}
884
+
885
+ should_run_nodes, site_area = (
886
+ (False, {}) if not all([land_use_type, has_no_prior_land_cover_data])
887
+ else (True, landCover_from_lookups) if landCover_from_lookups and all(landCover_from_lookups.values())
888
+ else (True, _COMPLETE_CHANGES_OTHER_LAND) if is_site_building
889
+ else _should_run_historical_land_use_change(
890
+ site=site,
891
+ nodes=relevant_nodes,
892
+ land_use_type=land_use_type
893
+ )
894
+ )
829
895
 
830
896
  logRequirements(site, model=MODEL, model_key=MODEL_KEY,
831
897
  has_management_nodes=bool(relevant_nodes),
832
898
  land_use_type=land_use_type,
833
- allowed_land_use_types=';'.join(ALLOWED_LAND_USE_TYPES),
899
+ allowed_land_use_types=';'.join(allowed_land_use_types),
834
900
  has_no_prior_land_cover_data=has_no_prior_land_cover_data,
835
901
  management_nodes=log_as_table([_omit(n, ['term']) for n in relevant_nodes]),
902
+ landCover_from_lookups=log_as_table([
903
+ {land_type: value} for land_type, value in landCover_from_lookups.items()
904
+ ]),
836
905
  should_run_nodes=should_run_nodes)
837
906
 
838
907
  should_run = all([land_use_type, has_no_prior_land_cover_data, should_run_nodes])