hestia-earth-models 0.61.7__py3-none-any.whl → 0.61.8__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 (43) hide show
  1. hestia_earth/models/cycle/completeness/electricityFuel.py +56 -0
  2. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +44 -59
  3. hestia_earth/models/geospatialDatabase/histosol.py +4 -0
  4. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +4 -2
  5. hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +1 -1
  6. hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +1 -1
  7. hestia_earth/models/ipcc2019/belowGroundCropResidue.py +1 -1
  8. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -1
  9. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +511 -458
  10. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +5 -1
  11. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +117 -3881
  12. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2060 -0
  13. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1630 -0
  14. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +324 -0
  15. hestia_earth/models/mocking/search-results.json +252 -252
  16. hestia_earth/models/site/organicCarbonPerHa.py +58 -44
  17. hestia_earth/models/site/soilMeasurement.py +18 -13
  18. hestia_earth/models/utils/__init__.py +28 -0
  19. hestia_earth/models/utils/array_builders.py +578 -0
  20. hestia_earth/models/utils/blank_node.py +2 -3
  21. hestia_earth/models/utils/descriptive_stats.py +285 -0
  22. hestia_earth/models/utils/emission.py +73 -2
  23. hestia_earth/models/utils/inorganicFertiliser.py +2 -2
  24. hestia_earth/models/utils/measurement.py +118 -4
  25. hestia_earth/models/version.py +1 -1
  26. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.61.8.dist-info}/METADATA +1 -1
  27. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.61.8.dist-info}/RECORD +43 -31
  28. tests/models/cycle/completeness/test_electricityFuel.py +21 -0
  29. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +2 -2
  30. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +54 -165
  31. tests/models/ipcc2019/test_organicCarbonPerHa.py +219 -460
  32. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +471 -0
  33. tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +208 -0
  34. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +75 -0
  35. tests/models/site/test_organicCarbonPerHa.py +3 -12
  36. tests/models/site/test_soilMeasurement.py +3 -18
  37. tests/models/utils/test_array_builders.py +253 -0
  38. tests/models/utils/test_descriptive_stats.py +134 -0
  39. tests/models/utils/test_emission.py +51 -1
  40. tests/models/utils/test_measurement.py +54 -2
  41. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.61.8.dist-info}/LICENSE +0 -0
  42. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.61.8.dist-info}/WHEEL +0 -0
  43. {hestia_earth_models-0.61.7.dist-info → hestia_earth_models-0.61.8.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,10 @@ from typing import Optional, Union
3
3
  from hestia_earth.schema import MeasurementMethodClassification
4
4
  from hestia_earth.utils.date import diff_in_days
5
5
  from hestia_earth.utils.model import find_term_match
6
- from hestia_earth.utils.tools import flatten, safe_parse_float
6
+ from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_float
7
7
 
8
8
  from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
9
- from hestia_earth.models.utils.blank_node import _get_last_date, group_nodes_by_last_date
9
+ from hestia_earth.models.utils.blank_node import _get_last_date, group_nodes_by_last_date, node_term_match
10
10
  from hestia_earth.models.utils.measurement import (
11
11
  _new_measurement, group_measurements_by_depth, measurement_value, OLDEST_DATE
12
12
  )
@@ -21,14 +21,17 @@ REQUIREMENTS = {
21
21
  "value": "",
22
22
  "term.@id": "soilBulkDensity",
23
23
  "depthUpper": "",
24
- "depthLower": ""
24
+ "depthLower": "",
25
+ "methodClassification": ["on-site physical measurement", "modelled using other measurements"]
25
26
  },
26
27
  {
27
28
  "@type": "Measurement",
28
29
  "value": "",
30
+ "dates": "",
29
31
  "term.@id": "organicCarbonPerKgSoil",
30
32
  "depthUpper": "",
31
- "depthLower": ""
33
+ "depthLower": "",
34
+ "methodClassification": ["on-site physical measurement", "modelled using other measurements"]
32
35
  }
33
36
  ]
34
37
  }
@@ -36,6 +39,7 @@ REQUIREMENTS = {
36
39
  RETURNS = {
37
40
  "Measurement": [{
38
41
  "value": "",
42
+ "dates": "",
39
43
  "depthUpper": "",
40
44
  "depthLower": "",
41
45
  "methodClassification": "modelled using other measurements"
@@ -51,6 +55,10 @@ RESCALE_DEPTH_LOWER = 30
51
55
  MAX_DEPTH_LOWER = 100
52
56
  SOIL_BULK_DENSITY_TERM_ID = 'soilBulkDensity'
53
57
  ORGANIC_CARBON_PER_KG_SOIL_TERM_ID = 'organicCarbonPerKgSoil'
58
+ VALID_MEASUREMENT_METHOD_CLASSIFICATIONS = {
59
+ MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT.value,
60
+ MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS.value
61
+ }
54
62
 
55
63
 
56
64
  def _measurement(
@@ -98,38 +106,40 @@ def _calc_organic_carbon_per_ha(
98
106
  return (depth_lower - depth_upper) * soil_bulk_density * organic_carbon_per_kg_soil * 100
99
107
 
100
108
 
101
- def _should_run_calculation_group(nodes: list) -> bool:
109
+ def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
102
110
  """
103
- Determines whether a depth interval group has sufficient data to calculate `organicCarbonPerHa` from
104
- `soilBulkDensity` and `organicCarbonPerKgSoil`.
111
+ Pre-process site data and determine whether there is sufficient data to calculate `organicCarbonPerHa`.
105
112
  """
106
- soilBulkDensity = find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, None)
107
- has_soil_bulk_density_depth_lower = (soilBulkDensity or {}).get('depthLower') is not None
108
- has_soil_bulk_density_depth_upper = (soilBulkDensity or {}).get('depthUpper') is not None
113
+ oc_nodes = [node for node in site.get("measurements", []) if _valid_measurement(node, TERM_ID)]
109
114
 
110
- organicCarbonPerKgSoil = find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, None)
111
- has_organic_carbon_per_kg_soil_depth_lower = (organicCarbonPerKgSoil or {}).get('depthLower') is not None
112
- has_organic_carbon_per_kg_soil_depth_upper = (organicCarbonPerKgSoil or {}).get('depthUpper') is not None
115
+ # We don't need to run the model for any dates we already have an `organicCarbonPerHa` value for.
116
+ oc_node_dates = set(non_empty_list(flatten(measurement.get("dates", []) for measurement in oc_nodes)))
113
117
 
114
- return all([
115
- has_soil_bulk_density_depth_lower,
116
- has_soil_bulk_density_depth_upper,
117
- has_organic_carbon_per_kg_soil_depth_lower,
118
- has_organic_carbon_per_kg_soil_depth_upper
119
- ])
118
+ occ_nodes = [
119
+ node for node in site.get("measurements", [])
120
+ if all([
121
+ _valid_measurement(node, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID),
122
+ len(node.get("dates", [])) > 0,
123
+ _get_last_date(node.get("dates", [])) not in oc_node_dates
124
+ ])
125
+ ]
120
126
 
127
+ bd_nodes = [node for node in site.get("measurements", []) if _valid_measurement(node, SOIL_BULK_DENSITY_TERM_ID)]
121
128
 
122
- def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
123
- """
124
- Pre-process site data and determine whether there is sufficient data to calculate `organicCarbonPerHa`.
125
- """
126
- grouped_measurements = {
127
- depth_key: nodes for depth_key, nodes in group_measurements_by_depth(site.get('measurements', [])).items()
129
+ measurements = occ_nodes + bd_nodes
130
+ grouped_measurements = group_measurements_by_depth(measurements, include_dates=False)
131
+
132
+ inventory = {
133
+ depth_key: {
134
+ "measurements": nodes,
135
+ "has-soil-bulk-density": bool(find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID)),
136
+ "has-organic-carbon-per-kg-soil": bool(find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID))
137
+ } for depth_key, nodes in grouped_measurements.items()
128
138
  }
129
139
 
130
140
  valid_grouped_measurements = {
131
- depth_key: nodes for depth_key, nodes in grouped_measurements.items()
132
- if _should_run_calculation_group(nodes)
141
+ depth_key: group["measurements"] for depth_key, group in inventory.items()
142
+ if all([group["has-soil-bulk-density"], group["has-organic-carbon-per-kg-soil"]])
133
143
  }
134
144
 
135
145
  should_run = bool(valid_grouped_measurements)
@@ -139,20 +149,24 @@ def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
139
149
  "inventory_calculation": log_as_table(
140
150
  {
141
151
  "depth-key": str(depth_key).replace("_", "-"),
142
- "should-run": depth_key in valid_grouped_measurements.keys(),
143
- "has-soil-bulk-density": (
144
- find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, {}).get('depthLower')
145
- and find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, {}).get('depthUpper')
146
- ),
147
- "has-organic-carbon-per-kg-soil": (
148
- find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, {}).get('depthLower')
149
- and find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, {}).get('depthUpper')
150
- )
151
- } for depth_key, nodes in grouped_measurements.items()
152
- ) or None
152
+ "should-run": depth_key in valid_grouped_measurements,
153
+ "has-soil-bulk-density": group["has-soil-bulk-density"],
154
+ "has-organic-carbon-per-kg-soil": group["has-organic-carbon-per-kg-soil"]
155
+ } for depth_key, group in inventory.items()
156
+ ) if inventory else "None"
153
157
  }
154
158
 
155
- return should_run, logs, valid_grouped_measurements
159
+ return should_run, valid_grouped_measurements, logs
160
+
161
+
162
+ def _valid_measurement(node: dict, target_term_id: str) -> bool:
163
+ return all([
164
+ node_term_match(node, target_term_id),
165
+ node.get("value"),
166
+ node.get("depthLower") is not None,
167
+ node.get("depthUpper") is not None,
168
+ node.get("methodClassification") in VALID_MEASUREMENT_METHOD_CLASSIFICATIONS
169
+ ])
156
170
 
157
171
 
158
172
  def _run_calculation(site: dict, depth_key: str, measurement_nodes: list[dict]) -> list[dict]:
@@ -332,11 +346,11 @@ def _should_run_rescale(organic_carbon_per_ha_nodes: list) -> tuple[bool, dict[s
332
346
  {
333
347
  "date": str(datestr),
334
348
  "should-run": datestr in valid_grouped_nodes.keys()
335
- } for datestr, nodes in grouped_nodes.items()
336
- ) or None
349
+ } for datestr in grouped_nodes.keys()
350
+ ) if grouped_nodes else "None"
337
351
  }
338
352
 
339
- return should_run, logs, valid_grouped_nodes
353
+ return should_run, valid_grouped_nodes, logs
340
354
 
341
355
 
342
356
  def _depth_distance(node: dict):
@@ -377,7 +391,7 @@ def _run_rescale(site: dict, organic_carbon_per_ha_nodes: list[dict]) -> list[di
377
391
 
378
392
 
379
393
  def run(site: dict):
380
- should_run_calculation, logs_calculation, grouped_measurements = _should_run_calculation(site)
394
+ should_run_calculation, grouped_measurements, logs_calculation = _should_run_calculation(site)
381
395
  result_calculation = (
382
396
  flatten([_run_calculation(site, depth_key, nodes) for depth_key, nodes in grouped_measurements.items()])
383
397
  if should_run_calculation else []
@@ -387,7 +401,7 @@ def run(site: dict):
387
401
  result_calculation + [m for m in site.get('measurements', []) if m.get('term', {}).get('@id') == TERM_ID]
388
402
  )
389
403
 
390
- should_run_rescale, logs_rescale, grouped_oc_per_ha_nodes = _should_run_rescale(oc_per_ha_nodes)
404
+ should_run_rescale, grouped_oc_per_ha_nodes, logs_rescale = _should_run_rescale(oc_per_ha_nodes)
391
405
  result_rescale = (
392
406
  [_run_rescale(site, nodes) for nodes in grouped_oc_per_ha_nodes.values()]
393
407
  if should_run_rescale else []
@@ -8,7 +8,7 @@ from copy import deepcopy
8
8
  from hestia_earth.schema import MeasurementMethodClassification
9
9
  from hestia_earth.utils.tools import non_empty_list, flatten
10
10
 
11
- from hestia_earth.models.log import logRequirements, logShouldRun, logErrorRun
11
+ from hestia_earth.models.log import logRequirements, logShouldRun, logErrorRun, log_as_table
12
12
  from hestia_earth.models.utils.measurement import _new_measurement
13
13
  from hestia_earth.models.utils.term import get_lookup_value
14
14
  from . import MODEL
@@ -20,7 +20,6 @@ REQUIREMENTS = {
20
20
  ]
21
21
  }
22
22
  }
23
-
24
23
  RETURNS = {
25
24
  "Measurement": [{
26
25
  "value": "",
@@ -30,11 +29,9 @@ RETURNS = {
30
29
  "methodClassification": "modelled using other measurements"
31
30
  }]
32
31
  }
33
-
34
32
  LOOKUPS = {
35
- "measurement": ["recommendAddingDepth", "depthSensitive"]
33
+ "measurement": "depthSensitive"
36
34
  }
37
-
38
35
  MODEL_KEY = 'soilMeasurement'
39
36
  STANDARD_DEPTHS = {(0, 30), (0, 50)}
40
37
 
@@ -154,17 +151,25 @@ def _get_depths_from_measurements(measurements: list) -> list:
154
151
  return needed_depths
155
152
 
156
153
 
157
- def _should_run(site: dict, model_key: str):
154
+ def _should_run(site: dict):
158
155
  # we only work with measurements with depths
159
- measurements = [m for m in site.get("measurements", []) if all([
160
- get_lookup_value(m.get("term", {}), LOOKUPS["measurement"][0], model=MODEL, model_key=model_key),
161
- m.get('value', [])
156
+ measurements = site.get("measurements", [])
157
+ measurement_sensitivity = {
158
+ m.get('term', {}).get('@id'): get_lookup_value(
159
+ m.get('term', {}), LOOKUPS["measurement"], model=MODEL, model_key=MODEL_KEY
160
+ )
161
+ for m in measurements
162
+ }
163
+ measurements_with_depths = [m for m in measurements if all([
164
+ not measurement_sensitivity[m.get("term", {}).get('@id')],
165
+ m.get('value', []),
166
+ "depthUpper" in m,
167
+ "depthLower" in m
162
168
  ])]
163
-
164
- measurements_with_depths = [m for m in measurements if "depthUpper" in m and "depthLower" in m]
165
169
  has_measurements_with_depths = len(measurements_with_depths) > 0
166
170
 
167
- logRequirements(site, model=MODEL, model_key=model_key,
171
+ logRequirements(site, model=MODEL, model_key=MODEL_KEY,
172
+ measurements_depth_sensitive=log_as_table(measurement_sensitivity),
168
173
  has_measurements_with_depths=has_measurements_with_depths)
169
174
 
170
175
  should_run = has_measurements_with_depths
@@ -175,5 +180,5 @@ def _should_run(site: dict, model_key: str):
175
180
 
176
181
 
177
182
  def run(site: dict):
178
- should_run, measurements_with_depths = _should_run(site=site, model_key=MODEL_KEY)
183
+ should_run, measurements_with_depths = _should_run(site)
179
184
  return non_empty_list(flatten(_run_harmonisation(measurements=measurements_with_depths))) if should_run else []
@@ -1,4 +1,6 @@
1
1
  from os.path import dirname, abspath
2
+ from collections.abc import Generator, Iterable
3
+ from itertools import tee
2
4
  import sys
3
5
  import datetime
4
6
  from functools import reduce
@@ -7,6 +9,7 @@ from typing import Any, Union
7
9
  from hestia_earth.schema import SchemaType
8
10
  from hestia_earth.utils.api import download_hestia
9
11
  from hestia_earth.utils.model import linked_node
12
+ from hestia_earth.utils.tools import flatten, non_empty_list
10
13
 
11
14
  from .constant import Units
12
15
 
@@ -137,3 +140,28 @@ def last_day_of_month(year: int, month: int):
137
140
  return datetime.date(int(year), 12, 31) if month == 12 else (
138
141
  datetime.date(int(year) + int(int(month) / 12), (int(month) % 12) + 1, 1) - datetime.timedelta(days=1)
139
142
  )
143
+
144
+
145
+ def flatten_args(args) -> list:
146
+ """
147
+ Flatten the input args into a single list.
148
+ """
149
+ return non_empty_list(flatten([list(arg) if is_iterable(arg) else [arg] for arg in args]))
150
+
151
+
152
+ def is_iterable(arg) -> bool:
153
+ """
154
+ Return `True` if the input arg is an instance of an `Iterable` (excluding `str` and `bytes`) or a `Generator`, else
155
+ return `False`.
156
+ """
157
+ return isinstance(arg, (Iterable, Generator)) and not isinstance(arg, (str, bytes))
158
+
159
+
160
+ def pairwise(iterable):
161
+ """
162
+ from https://docs.python.org/3.9/library/itertools.html#itertools-recipes
163
+ s -> (s0,s1), (s1,s2), (s2, s3), ...
164
+ """
165
+ a, b = tee(iterable)
166
+ next(b, None)
167
+ return zip(a, b)