hestia-earth-models 0.71.0__py3-none-any.whl → 0.72.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,6 +88,14 @@
88
88
  "runStrategy": "add_blank_node_if_missing",
89
89
  "mergeStrategy": "list",
90
90
  "stage": 1
91
+ },
92
+ {
93
+ "key": "measurements",
94
+ "model": "hestia",
95
+ "value": "histosol",
96
+ "runStrategy": "add_blank_node_if_missing",
97
+ "mergeStrategy": "list",
98
+ "stage": 1
91
99
  }
92
100
  ],
93
101
  [
@@ -1,8 +1,7 @@
1
- from typing import List, Optional, Tuple
2
-
3
1
  from hestia_earth.schema import TermTermType
4
2
  from hestia_earth.utils.model import filter_list_term_type
5
3
  from hestia_earth.utils.tools import list_sum
4
+ from typing import List, Optional, Tuple
6
5
 
7
6
  from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table, debugValues
8
7
  from hestia_earth.models.utils.blank_node import get_lookup_value
@@ -89,6 +88,23 @@ def _indicator_factors(impact_assessment: dict, indicator: dict):
89
88
  }
90
89
 
91
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
+ def _count_duplicate_indicators(reference_indicator: dict, indicators: list) -> int:
96
+ """
97
+ Counts the number of `reference_indicator` indicators found in a list of indicators.
98
+ Uses indicator.term.@id and indicator.inputs to determine uniqueness.
99
+ """
100
+ return sum([
101
+ 1
102
+ for i in indicators
103
+ if (i["term"]["@id"] == reference_indicator["term"]["@id"]) and (
104
+ _map_input_ids(i) == _map_input_ids(reference_indicator))
105
+ ])
106
+
107
+
92
108
  def _indicator(value: float) -> dict:
93
109
  indicator = _new_indicator(TERM_ID, MODEL)
94
110
  indicator['value'] = value
@@ -115,7 +131,7 @@ def _should_run(impact_assessment: dict) -> Tuple[bool, list[dict]]:
115
131
  processed_indicators = [{
116
132
  "indicator": indicator['term']['@id'],
117
133
  "valid-indicator": _valid_indicator(indicator),
118
- "one-indicator-for-category": sum(1 for i in indicators if i['term']['@id'] == indicator['term']['@id']) == 1,
134
+ "one-indicator-for-category": _count_duplicate_indicators(indicator, indicators) == 1,
119
135
  "indicator-pef-category": indicator.get('term', {}).get('@id'),
120
136
  } | _indicator_factors(impact_assessment, indicator) for indicator in indicators]
121
137
 
@@ -1,7 +1,7 @@
1
- from hestia_earth.schema import MeasurementMethodClassification, TermTermType
1
+ from hestia_earth.schema import MeasurementMethodClassification
2
2
 
3
3
  from hestia_earth.models.log import logRequirements, logShouldRun
4
- from hestia_earth.models.utils.measurement import _new_measurement
4
+ from hestia_earth.models.utils.measurement import _new_measurement, total_other_soilType_value
5
5
  from hestia_earth.models.utils.source import get_source
6
6
  from .utils import download, has_geospatial_data, should_download
7
7
  from . import MODEL
@@ -14,7 +14,13 @@ REQUIREMENTS = {
14
14
  {"region": {"@type": "Term", "termType": "region"}}
15
15
  ],
16
16
  "none": {
17
- "measurements": [{"@type": "Measurement", "value": "", "term.termType": "soilType"}]
17
+ "measurements": [{
18
+ "@type": "Measurement",
19
+ "value": "100",
20
+ "depthUpper": "0",
21
+ "depthLower": "30",
22
+ "term.termType": "soilType"
23
+ }]
18
24
  }
19
25
  }
20
26
  }
@@ -50,17 +56,18 @@ def _run(site: dict):
50
56
 
51
57
 
52
58
  def _should_run(site: dict):
53
- measurements = site.get('measurements', [])
54
- no_soil_type = all([m.get('term', {}).get('termType') != TermTermType.SOILTYPE.value for m in measurements])
55
59
  contains_geospatial_data = has_geospatial_data(site)
56
60
  below_max_area_size = should_download(TERM_ID, site)
57
61
 
62
+ total_measurements_value = total_other_soilType_value(site.get('measurements', []), TERM_ID)
63
+
58
64
  logRequirements(site, model=MODEL, term=TERM_ID,
59
65
  contains_geospatial_data=contains_geospatial_data,
60
66
  below_max_area_size=below_max_area_size,
61
- no_soil_type=no_soil_type)
67
+ total_soilType_measurements_value=total_measurements_value,
68
+ total_soilType_measurements_value_is_0=total_measurements_value == 0)
62
69
 
63
- should_run = all([contains_geospatial_data, below_max_area_size, no_soil_type])
70
+ should_run = all([contains_geospatial_data, below_max_area_size, total_measurements_value == 0])
64
71
  logShouldRun(site, MODEL, TERM_ID, should_run)
65
72
  return should_run
66
73
 
@@ -0,0 +1,53 @@
1
+ from hestia_earth.schema import MeasurementMethodClassification
2
+
3
+ from hestia_earth.models.log import logRequirements, logShouldRun
4
+ from hestia_earth.models.utils.measurement import _new_measurement, total_other_soilType_value
5
+ from . import MODEL
6
+
7
+ REQUIREMENTS = {
8
+ "Site": {
9
+ "measurements": [{
10
+ "@type": "Measurement",
11
+ "value": "100",
12
+ "depthUpper": "0",
13
+ "depthLower": "30",
14
+ "term.termType": "soilType"
15
+ }]
16
+ }
17
+ }
18
+ RETURNS = {
19
+ "Measurement": [{
20
+ "value": "0",
21
+ "depthUpper": "0",
22
+ "depthLower": "30",
23
+ "methodClassification": "modelled using other measurements"
24
+ }]
25
+ }
26
+ LOOKUPS = {
27
+ "soilType": "sumMax100Group"
28
+ }
29
+ TERM_ID = 'histosol'
30
+
31
+
32
+ def _measurement():
33
+ measurement = _new_measurement(TERM_ID)
34
+ measurement['value'] = [0]
35
+ measurement['depthUpper'] = 0
36
+ measurement['depthLower'] = 30
37
+ measurement['methodClassification'] = MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS.value
38
+ return measurement
39
+
40
+
41
+ def _should_run(site: dict):
42
+ total_measurements_value = total_other_soilType_value(site.get('measurements', []), TERM_ID)
43
+
44
+ logRequirements(site, model=MODEL, term=TERM_ID,
45
+ total_soilType_measurements_value=total_measurements_value,
46
+ total_soilType_measurements_value_is_100=total_measurements_value == 100)
47
+
48
+ should_run = all([total_measurements_value == 100])
49
+ logShouldRun(site, MODEL, TERM_ID, should_run)
50
+ return should_run
51
+
52
+
53
+ def run(site: dict): return [_measurement()] if _should_run(site) else []
@@ -148,7 +148,7 @@ def cap_values(dictionary: dict, lower_limit: float = 0, upper_limit: float = 1)
148
148
 
149
149
  def site_area_sum_to_100(dict_of_percentages: dict):
150
150
  return False if dict_of_percentages == {} else (
151
- math.isclose(sum(dict_of_percentages.values()), 1.0, rel_tol=0.01) or
151
+ math.isclose(sum(dict_of_percentages.values()), 1.0, rel_tol=0.05) or
152
152
  math.isclose(sum(dict_of_percentages.values()), 0.0, rel_tol=0.01)
153
153
  )
154
154
 
@@ -581,6 +581,14 @@ def _get_year_from_landCover(node: dict):
581
581
  return int(date[:4])
582
582
 
583
583
 
584
+ def _scale_site_area_errors(site_area: dict) -> dict:
585
+ """Redistribute the result of any rounding error in proportion to the other land use types."""
586
+ # Positive errors would not have been capped, so won't be missing.
587
+ negative_errors = [v for v in site_area.values() if v < 0.0]
588
+ return {k: v + negative_errors[0] * v for k, v in site_area.items()} \
589
+ if negative_errors and abs(negative_errors[0]) < 1 and all([v < 1 for v in site_area.values()]) else site_area
590
+
591
+
584
592
  def _should_run_historical_land_use_change(site: dict, nodes: list, land_use_type: str) -> tuple[bool, dict]:
585
593
  # Assume a single management node for single-cropping.
586
594
  return _should_run_historical_land_use_change_single_crop(
@@ -728,7 +736,7 @@ def _should_run_historical_land_use_change_single_crop(
728
736
  if land_type != land_use_type
729
737
  }
730
738
  site_area[land_use_type] = 1 - sum(site_area.values())
731
- capped_site_area = cap_values(dictionary=site_area, lower_limit=0, upper_limit=1)
739
+ capped_site_area = cap_values(dictionary=_scale_site_area_errors(site_area))
732
740
 
733
741
  sum_of_site_areas_is_100 = site_area_sum_to_100(capped_site_area)
734
742
  site_type_allowed = site.get("siteType") in SITE_TYPES
@@ -2,7 +2,8 @@ from hestia_earth.schema import EmissionMethodTier
2
2
  from hestia_earth.utils.tools import list_sum, safe_parse_float, non_empty_list
3
3
  from hestia_earth.utils.model import find_term_match
4
4
 
5
- from hestia_earth.models.log import debugValues, logRequirements, logShouldRun
5
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
6
+ from hestia_earth.models.utils import multiply_values
6
7
  from hestia_earth.models.utils.completeness import _is_term_type_complete
7
8
  from hestia_earth.models.utils.emission import _new_emission
8
9
  from hestia_earth.models.utils.term import get_urea_terms
@@ -36,66 +37,65 @@ def _emission(value: float):
36
37
  return emission
37
38
 
38
39
 
39
- def _urea_input_value(cycle: dict):
40
- def exec(data: dict):
41
- term_id = data.get('id')
42
- values = data.get('values')
43
- coeff = safe_parse_float(get_term_lookup(term_id, LOOKUPS['inorganicFertiliser'][2]), 1)
44
- debugValues(cycle, model=MODEL, term=TERM_ID,
45
- product=term_id,
46
- coefficient=coeff)
47
- return list_sum(values) * coeff
48
- return exec
40
+ def _urea_emission_factor(term_id: str):
41
+ return safe_parse_float(get_term_lookup(term_id, LOOKUPS['inorganicFertiliser'][2]), None)
49
42
 
50
43
 
51
- def _run(cycle: dict, urea_values: list):
52
- value = list_sum(list(map(_urea_input_value(cycle), urea_values)))
44
+ def _run(urea_values: list):
45
+ value = list_sum([v.get('value') * v.get('factor') for v in urea_values if v.get('value')])
53
46
  return [_emission(value)]
54
47
 
55
48
 
56
- def _get_urea_values(cycle: dict, inputs: list, term_id: str):
49
+ def _get_urea_value(cycle: dict, inputs: list, term_id: str):
57
50
  inputs = list(filter(lambda i: i.get('term', {}).get('@id') == term_id, inputs))
58
51
  values = [list_sum(i.get('value'), 0) for i in inputs if len(i.get('value', [])) > 0]
59
- return [0] if len(inputs) == 0 and _is_term_type_complete(cycle, 'fertiliser') else values
52
+ return list_sum(values, default=None)
60
53
 
61
54
 
62
55
  def _should_run(cycle: dict):
56
+ is_fertiliser_complete = _is_term_type_complete(cycle, 'fertiliser')
63
57
  inputs = cycle.get('inputs', [])
64
58
  term_ids = get_urea_terms()
65
59
 
66
60
  country_id = cycle.get('site', {}).get('country', {}).get('@id')
67
61
  urea_share = get_country_breakdown(MODEL, TERM_ID, country_id, LOOKUPS['inorganicFertiliser'][0])
68
62
  uan_share = get_country_breakdown(MODEL, TERM_ID, country_id, LOOKUPS['inorganicFertiliser'][1])
69
- urea_unspecified_as_n = list_sum(find_term_match(inputs, UNSPECIFIED_TERM_ID).get('value', []))
63
+ urea_unspecified_as_n = list_sum(find_term_match(inputs, UNSPECIFIED_TERM_ID).get('value', []), default=None)
70
64
 
71
65
  urea_values = [
72
66
  {
73
- 'id': id,
74
- 'values': _get_urea_values(cycle, inputs, id)
75
- } for id in term_ids
67
+ 'id': term_id,
68
+ 'value': _get_urea_value(cycle, inputs, term_id),
69
+ 'factor': _urea_emission_factor(term_id)
70
+ } for term_id in term_ids
76
71
  ] + non_empty_list([
77
72
  {
78
73
  'id': 'ureaKgN',
79
- 'values': [urea_unspecified_as_n * urea_share]
80
- } if urea_share is not None else None,
74
+ 'value': multiply_values([urea_unspecified_as_n, urea_share]),
75
+ 'factor': _urea_emission_factor('ureaKgN')
76
+ },
81
77
  {
82
78
  'id': 'ureaAmmoniumNitrateKgN',
83
- 'values': [urea_unspecified_as_n * uan_share]
84
- } if urea_share is not None else None
85
- ] if urea_unspecified_as_n > 0 else [])
86
- has_urea_value = any([len(data.get('values')) > 0 for data in urea_values])
79
+ 'value': multiply_values([urea_unspecified_as_n, uan_share]),
80
+ 'factor': _urea_emission_factor('ureaAmmoniumNitrateKgN')
81
+ }
82
+ ] if urea_unspecified_as_n is not None else [])
83
+
84
+ has_urea_value = any([data.get('value') is not None for data in urea_values])
87
85
 
88
86
  logRequirements(cycle, model=MODEL, term=TERM_ID,
87
+ is_term_type_fertiliser_complete=is_fertiliser_complete,
89
88
  has_urea_value=has_urea_value,
89
+ urea_values=log_as_table(urea_values),
90
90
  urea_unspecified_as_n=urea_unspecified_as_n,
91
91
  urea_share=urea_share,
92
92
  uan_share=uan_share)
93
93
 
94
- should_run = all([has_urea_value])
94
+ should_run = has_urea_value or is_fertiliser_complete
95
95
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
96
96
  return should_run, urea_values
97
97
 
98
98
 
99
99
  def run(cycle: dict):
100
100
  should_run, urea_values = _should_run(cycle)
101
- return _run(cycle, urea_values) if should_run else []
101
+ return _run(urea_values) if should_run else []
@@ -3,17 +3,18 @@ from functools import reduce
3
3
  from numpy import empty_like, random, vstack
4
4
  from numpy.typing import NDArray
5
5
  from pydash.objects import merge
6
- from typing import Callable, Optional, Union
6
+ from typing import Callable, Literal, Optional, Union
7
7
 
8
8
  from hestia_earth.schema import MeasurementMethodClassification, SiteSiteType, TermTermType
9
- from hestia_earth.utils.model import find_term_match, filter_list_term_type
10
9
  from hestia_earth.utils.blank_node import get_node_value
10
+ from hestia_earth.utils.model import find_term_match, filter_list_term_type
11
+ from hestia_earth.utils.tools import non_empty_list
11
12
 
12
13
  from hestia_earth.models.utils import split_on_condition
13
14
  from hestia_earth.models.utils.array_builders import gen_seed
14
15
  from hestia_earth.models.utils.blank_node import (
15
- cumulative_nodes_match, cumulative_nodes_lookup_match, cumulative_nodes_term_match, node_lookup_match,
16
- node_term_match, group_nodes_by_year, validate_start_date_end_date
16
+ cumulative_nodes_match, cumulative_nodes_lookup_match, cumulative_nodes_term_match, group_by_term,
17
+ node_lookup_match, node_term_match, group_nodes_by_year, validate_start_date_end_date
17
18
  )
18
19
  from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
19
20
  from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
@@ -1031,8 +1032,8 @@ def _assign_ipcc_soil_category(
1031
1032
  IpccSoilCategory
1032
1033
  The assigned IPCC soil category.
1033
1034
  """
1034
- soil_types = filter_list_term_type(measurement_nodes, TermTermType.SOILTYPE)
1035
- usda_soil_types = filter_list_term_type(measurement_nodes, TermTermType.USDASOILTYPE)
1035
+ soil_types = _get_soil_type_measurements(measurement_nodes, TermTermType.SOILTYPE)
1036
+ usda_soil_types = _get_soil_type_measurements(measurement_nodes, TermTermType.USDASOILTYPE)
1036
1037
 
1037
1038
  clay_content = get_node_value(find_term_match(measurement_nodes, _CLAY_CONTENT_TERM_ID))
1038
1039
  sand_content = get_node_value(find_term_match(measurement_nodes, _SAND_CONTENT_TERM_ID))
@@ -1053,6 +1054,20 @@ def _assign_ipcc_soil_category(
1053
1054
  ) if len(soil_types) > 0 or len(usda_soil_types) > 0 else default
1054
1055
 
1055
1056
 
1057
+ def _get_soil_type_measurements(
1058
+ nodes: list[dict], term_type: Literal[TermTermType.SOILTYPE, TermTermType.USDASOILTYPE]
1059
+ ) -> list[dict]:
1060
+ grouped = group_by_term(filter_list_term_type(nodes, term_type))
1061
+
1062
+ def depth_distance(node):
1063
+ upper, lower = node.get("depthUpper", 0), node.get("depthLower", 100)
1064
+ return abs(upper - DEPTH_UPPER) + abs(lower - DEPTH_LOWER)
1065
+
1066
+ return non_empty_list(
1067
+ min(nodes_, key=depth_distance) for key in grouped if (nodes_ := grouped.get(key, []))
1068
+ )
1069
+
1070
+
1056
1071
  def _check_soil_category(
1057
1072
  *,
1058
1073
  key: IpccSoilCategory,
@@ -1461,7 +1476,7 @@ Value: Corresponding decision tree for IPCC management categories based on land
1461
1476
  """
1462
1477
 
1463
1478
  _IPCC_LAND_USE_CATEGORY_TO_DEFAULT_IPCC_MANAGEMENT_CATEGORY = {
1464
- IpccLandUseCategory.GRASSLAND: IpccManagementCategory.NOMINALLY_MANAGED,
1479
+ IpccLandUseCategory.GRASSLAND: IpccManagementCategory.UNKNOWN,
1465
1480
  IpccLandUseCategory.ANNUAL_CROPS_WET: IpccManagementCategory.UNKNOWN,
1466
1481
  IpccLandUseCategory.ANNUAL_CROPS: IpccManagementCategory.UNKNOWN
1467
1482
  }