hestia-earth-models 0.57.2__py3-none-any.whl → 0.59.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 (109) hide show
  1. hestia_earth/models/cycle/aboveGroundCropResidueTotal.py +17 -12
  2. hestia_earth/models/cycle/excretaKgMass.py +4 -5
  3. hestia_earth/models/cycle/excretaKgN.py +4 -5
  4. hestia_earth/models/cycle/excretaKgVs.py +4 -5
  5. hestia_earth/models/cycle/inorganicFertiliser.py +2 -2
  6. hestia_earth/models/cycle/{irrigated.py → irrigatedTypeUnspecified.py} +4 -4
  7. hestia_earth/models/cycle/liveAnimal.py +9 -11
  8. hestia_earth/models/cycle/milkYield.py +154 -0
  9. hestia_earth/models/cycle/residueIncorporated.py +1 -1
  10. hestia_earth/models/cycle/utils.py +6 -0
  11. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +3 -3
  12. hestia_earth/models/faostat2018/seed.py +2 -3
  13. hestia_earth/models/geospatialDatabase/clayContent.py +17 -4
  14. hestia_earth/models/geospatialDatabase/sandContent.py +17 -4
  15. hestia_earth/models/geospatialDatabase/siltContent.py +2 -2
  16. hestia_earth/models/impact_assessment/irrigated.py +0 -3
  17. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +2 -2
  18. hestia_earth/models/ipcc2006/n2OToAirCropResidueDecompositionIndirect.py +2 -2
  19. hestia_earth/models/ipcc2006/n2OToAirExcretaDirect.py +1 -1
  20. hestia_earth/models/ipcc2006/n2OToAirExcretaIndirect.py +8 -4
  21. hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserDirect.py +4 -1
  22. hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserIndirect.py +1 -1
  23. hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserDirect.py +1 -1
  24. hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserIndirect.py +1 -1
  25. hestia_earth/models/ipcc2006/utils.py +11 -8
  26. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +4 -4
  27. hestia_earth/models/ipcc2019/ch4ToAirFloodedRice.py +16 -7
  28. hestia_earth/models/ipcc2019/co2ToAirSoilCarbonStockChangeManagementChange.py +759 -0
  29. hestia_earth/models/ipcc2019/croppingDuration.py +12 -6
  30. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +5 -52
  31. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserDirect.py +104 -0
  32. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +1 -1
  33. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserDirect.py +105 -0
  34. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +1 -1
  35. hestia_earth/models/ipcc2019/no3ToGroundwaterCropResidueDecomposition.py +1 -1
  36. hestia_earth/models/ipcc2019/no3ToGroundwaterExcreta.py +1 -1
  37. hestia_earth/models/ipcc2019/no3ToGroundwaterInorganicFertiliser.py +1 -1
  38. hestia_earth/models/ipcc2019/no3ToGroundwaterOrganicFertiliser.py +1 -1
  39. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +1088 -1268
  40. hestia_earth/models/ipcc2019/pastureGrass.py +4 -4
  41. hestia_earth/models/ipcc2019/utils.py +102 -1
  42. hestia_earth/models/koble2014/aboveGroundCropResidue.py +15 -17
  43. hestia_earth/models/koble2014/cropResidueManagement.py +2 -2
  44. hestia_earth/models/koble2014/utils.py +19 -3
  45. hestia_earth/models/linkedImpactAssessment/__init__.py +4 -2
  46. hestia_earth/models/log.py +15 -3
  47. hestia_earth/models/mocking/search-results.json +184 -118
  48. hestia_earth/models/pooreNemecek2018/excretaKgN.py +6 -7
  49. hestia_earth/models/pooreNemecek2018/excretaKgVs.py +7 -6
  50. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterCropResidueDecomposition.py +3 -2
  51. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterExcreta.py +3 -2
  52. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterInorganicFertiliser.py +3 -2
  53. hestia_earth/models/pooreNemecek2018/saplings.py +0 -1
  54. hestia_earth/models/site/management.py +168 -0
  55. hestia_earth/models/site/organicCarbonPerHa.py +251 -89
  56. hestia_earth/models/stehfestBouwman2006/n2OToAirCropResidueDecompositionDirect.py +3 -2
  57. hestia_earth/models/stehfestBouwman2006/n2OToAirExcretaDirect.py +3 -2
  58. hestia_earth/models/stehfestBouwman2006/n2OToAirInorganicFertiliserDirect.py +3 -2
  59. hestia_earth/models/stehfestBouwman2006/n2OToAirOrganicFertiliserDirect.py +3 -2
  60. hestia_earth/models/stehfestBouwman2006/noxToAirCropResidueDecomposition.py +3 -2
  61. hestia_earth/models/stehfestBouwman2006/noxToAirExcreta.py +3 -2
  62. hestia_earth/models/stehfestBouwman2006/noxToAirInorganicFertiliser.py +3 -2
  63. hestia_earth/models/stehfestBouwman2006/noxToAirOrganicFertiliser.py +3 -2
  64. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirCropResidueDecomposition.py +3 -2
  65. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirExcreta.py +3 -2
  66. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirInorganicFertiliser.py +3 -2
  67. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirOrganicFertiliser.py +3 -2
  68. hestia_earth/models/utils/aggregated.py +1 -0
  69. hestia_earth/models/utils/blank_node.py +394 -72
  70. hestia_earth/models/utils/cropResidue.py +13 -0
  71. hestia_earth/models/utils/cycle.py +18 -9
  72. hestia_earth/models/utils/measurement.py +1 -1
  73. hestia_earth/models/utils/property.py +4 -4
  74. hestia_earth/models/utils/term.py +48 -3
  75. hestia_earth/models/version.py +1 -1
  76. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/METADATA +5 -9
  77. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/RECORD +109 -97
  78. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/WHEEL +1 -1
  79. tests/models/cycle/animal/input/test_hestiaAggregatedData.py +2 -14
  80. tests/models/cycle/input/test_hestiaAggregatedData.py +4 -16
  81. tests/models/cycle/test_coldCarcassWeightPerHead.py +1 -1
  82. tests/models/cycle/test_coldDressedCarcassWeightPerHead.py +1 -1
  83. tests/models/cycle/{test_irrigated.py → test_irrigatedTypeUnspecified.py} +1 -1
  84. tests/models/cycle/test_milkYield.py +58 -0
  85. tests/models/cycle/test_readyToCookWeightPerHead.py +1 -1
  86. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +1 -1
  87. tests/models/geospatialDatabase/test_clayContent.py +9 -3
  88. tests/models/geospatialDatabase/test_sandContent.py +9 -3
  89. tests/models/ipcc2006/test_n2OToAirExcretaDirect.py +7 -2
  90. tests/models/ipcc2006/test_n2OToAirExcretaIndirect.py +1 -1
  91. tests/models/ipcc2006/test_n2OToAirInorganicFertiliserDirect.py +7 -2
  92. tests/models/ipcc2006/test_n2OToAirInorganicFertiliserIndirect.py +7 -2
  93. tests/models/ipcc2006/test_n2OToAirOrganicFertiliserDirect.py +7 -2
  94. tests/models/ipcc2006/test_n2OToAirOrganicFertiliserIndirect.py +7 -2
  95. tests/models/ipcc2019/test_ch4ToAirEntericFermentation.py +1 -1
  96. tests/models/ipcc2019/test_co2ToAirSoilCarbonStockChangeManagementChange.py +228 -0
  97. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserDirect.py +74 -0
  98. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserDirect.py +74 -0
  99. tests/models/ipcc2019/test_organicCarbonPerHa.py +303 -1044
  100. tests/models/koble2014/test_residueBurnt.py +1 -2
  101. tests/models/koble2014/test_residueLeftOnField.py +1 -2
  102. tests/models/koble2014/test_residueRemoved.py +1 -2
  103. tests/models/koble2014/test_utils.py +52 -0
  104. tests/models/site/test_management.py +117 -0
  105. tests/models/site/test_organicCarbonPerHa.py +51 -5
  106. tests/models/utils/test_blank_node.py +230 -34
  107. tests/models/utils/test_term.py +17 -3
  108. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/LICENSE +0 -0
  109. {hestia_earth_models-0.57.2.dist-info → hestia_earth_models-0.59.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,16 @@
1
1
  from typing import Optional
2
2
 
3
3
  from hestia_earth.schema import MeasurementMethodClassification
4
+ from hestia_earth.utils.date import diff_in_days
4
5
  from hestia_earth.utils.model import find_term_match
6
+ from hestia_earth.utils.tools import flatten, safe_parse_float
5
7
 
6
- from hestia_earth.models.log import logRequirements, logShouldRun
7
- from hestia_earth.models.utils.source import get_source
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
8
10
  from hestia_earth.models.utils.measurement import (
9
- _new_measurement, group_measurements_by_depth, _group_measurement_key, measurement_value
11
+ _new_measurement, group_measurements_by_depth, measurement_value, OLDEST_DATE
10
12
  )
13
+ from hestia_earth.models.utils.source import get_source
11
14
  from . import MODEL
12
15
 
13
16
  REQUIREMENTS = {
@@ -43,6 +46,12 @@ BIBLIO_TITLE = 'Soil organic carbon sequestration rates in vineyard agroecosyste
43
46
  RESCALE_DEPTH_UPPER = 0
44
47
  RESCALE_DEPTH_LOWER = 30
45
48
 
49
+ # --- UTILS ---
50
+
51
+ MAX_DEPTH_LOWER = 100
52
+ SOIL_BULK_DENSITY_TERM_ID = 'soilBulkDensity'
53
+ ORGANIC_CARBON_PER_KG_SOIL_TERM_ID = 'organicCarbonPerKgSoil'
54
+
46
55
 
47
56
  def _measurement(site: dict, value: float, depthUpper: int, depthLower: int, date: Optional[str] = None):
48
57
  data = _new_measurement(TERM_ID)
@@ -55,14 +64,156 @@ def _measurement(site: dict, value: float, depthUpper: int, depthLower: int, dat
55
64
  return data | get_source(site, BIBLIO_TITLE)
56
65
 
57
66
 
58
- def _should_run_rescale(measurements: list):
59
- return any([m for m in measurements if all([
60
- m.get('depthUpper', 1) == RESCALE_DEPTH_UPPER,
61
- m.get('depthLower', 101) <= 100
62
- ])]) and not any([m for m in measurements if all([
63
- m.get('depthUpper', 1) == RESCALE_DEPTH_UPPER,
64
- m.get('depthLower', 101) == RESCALE_DEPTH_LOWER
65
- ])])
67
+ # --- CALCULATE `organicCarbonPerHa` ---
68
+
69
+
70
+ def _calc_organic_carbon_per_ha(
71
+ depth_upper: float,
72
+ depth_lower: float,
73
+ soil_bulk_density: float,
74
+ organic_carbon_per_kg_soil: float
75
+ ) -> float:
76
+ """
77
+ Calculate `organicCarbonPerHa` from `soilBulkDensity` and `organicCarbonPerKgSoil` using method adapted from
78
+ [Payen et al (2021)](https://doi.org/10.1016/j.jclepro.2020.125736).
79
+
80
+ Parameters
81
+ ----------
82
+ depth_upper : float
83
+ Measurement depth upper in centimetres (min `0`).
84
+ depth_lower : float,
85
+ Measurement depth upper in centimetres (min `0`).
86
+ soil_bulk_density : float,
87
+ Soil bulk density between depth upper and depth lower, Mg soil m-3
88
+ organic_carbon_per_kg_soil : float
89
+ Soil organic carbon concentration between depth upper and depth lower, kg C kg soil-1
90
+
91
+ Return
92
+ ------
93
+ float
94
+ The SOC stock per hectare within the specified depth interval, kg C ha-1.
95
+ """
96
+ return (depth_lower - depth_upper) * soil_bulk_density * organic_carbon_per_kg_soil * 100
97
+
98
+
99
+ def _should_run_calculation_group(nodes: list) -> bool:
100
+ """
101
+ Determines whether a depth interval group has sufficient data to calculate `organicCarbonPerHa` from
102
+ `soilBulkDensity` and `organicCarbonPerKgSoil`.
103
+ """
104
+ soilBulkDensity = find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, None)
105
+ has_soil_bulk_density_depth_lower = (soilBulkDensity or {}).get('depthLower') is not None
106
+ has_soil_bulk_density_depth_upper = (soilBulkDensity or {}).get('depthUpper') is not None
107
+
108
+ organicCarbonPerKgSoil = find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, None)
109
+ has_organic_carbon_per_kg_soil_depth_lower = (organicCarbonPerKgSoil or {}).get('depthLower') is not None
110
+ has_organic_carbon_per_kg_soil_depth_upper = (organicCarbonPerKgSoil or {}).get('depthUpper') is not None
111
+
112
+ return all([
113
+ has_soil_bulk_density_depth_lower,
114
+ has_soil_bulk_density_depth_upper,
115
+ has_organic_carbon_per_kg_soil_depth_lower,
116
+ has_organic_carbon_per_kg_soil_depth_upper
117
+ ])
118
+
119
+
120
+ def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
121
+ """
122
+ Pre-process site data and determine whether there is sufficient data to calculate `organicCarbonPerHa`.
123
+ """
124
+ grouped_measurements = {
125
+ depth_key: nodes for depth_key, nodes in group_measurements_by_depth(site.get('measurements', [])).items()
126
+ }
127
+
128
+ valid_grouped_measurements = {
129
+ depth_key: nodes for depth_key, nodes in grouped_measurements.items()
130
+ if _should_run_calculation_group(nodes)
131
+ }
132
+
133
+ should_run = bool(valid_grouped_measurements)
134
+
135
+ logs = {
136
+ "should_run_calculation": should_run,
137
+ "inventory_calculation": log_as_table(
138
+ {
139
+ "depth-key": str(depth_key).replace("_", "-"),
140
+ "should-run": depth_key in valid_grouped_measurements.keys(),
141
+ "has-soil-bulk-density": (
142
+ find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, {}).get('depthLower')
143
+ and find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, {}).get('depthUpper')
144
+ ),
145
+ "has-organic-carbon-per-kg-soil": (
146
+ find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, {}).get('depthLower')
147
+ and find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, {}).get('depthUpper')
148
+ )
149
+ } for depth_key, nodes in grouped_measurements.items()
150
+ ) or None
151
+ }
152
+
153
+ return should_run, logs, valid_grouped_measurements
154
+
155
+
156
+ def _run_calculation(site: dict, depth_key: str, measurement_nodes: list[dict]) -> list[dict]:
157
+ """
158
+ Returns an `organicCarbonPerHa` measurement node for each `organicCarbonPerKgSoil` node in depth group using the
159
+ most relevant `soilBulkDensity` node available.
160
+
161
+ Parameters
162
+ ----------
163
+ site : dict
164
+ A [Site node](https://www.hestia.earth/schema/Site).
165
+ depth_key : str
166
+ A depth key in the format `"a_to_b"`.
167
+ measurement_nodes : list[dict]
168
+ A list of pre-validated [Measurement nodes](https://www.hestia.earth/schema/Measurement).
169
+
170
+ Return
171
+ ------
172
+ list[dict]
173
+ A list of `organicCarbonPerHa` [Measurement nodes](https://www.hestia.earth/schema/Measurement).
174
+ """
175
+ split = depth_key.split('_') # "a_to_b"
176
+ depth_upper = safe_parse_float(split[0])
177
+ depth_lower = safe_parse_float(split[2])
178
+
179
+ soil_bulk_density_nodes = [
180
+ node for node in measurement_nodes if node.get('term', {}).get('@id') == SOIL_BULK_DENSITY_TERM_ID
181
+ ]
182
+
183
+ organic_carbon_per_kg_soil_nodes = [
184
+ node for node in measurement_nodes if node.get('term', {}).get('@id') == ORGANIC_CARBON_PER_KG_SOIL_TERM_ID
185
+ ]
186
+
187
+ dates = [_get_last_date(node.get('dates', [])) for node in organic_carbon_per_kg_soil_nodes]
188
+
189
+ def closest_bd(datestr: str):
190
+ """
191
+ Returns the `soilBulkDensity` node closest to target datestr. `nodes` input are pre-validated to always contain
192
+ at least one `soilBulkDensity` node.
193
+ """
194
+ return next(
195
+ iter(sorted(
196
+ soil_bulk_density_nodes,
197
+ key=lambda node: abs(diff_in_days(
198
+ _get_last_date(node.get('dates', [])) or OLDEST_DATE,
199
+ datestr or OLDEST_DATE
200
+ ))
201
+ ))
202
+ )
203
+
204
+ values = [
205
+ _calc_organic_carbon_per_ha(
206
+ depth_upper,
207
+ depth_lower,
208
+ measurement_value(closest_bd(datestr)),
209
+ measurement_value(organic_carbon_per_kg_soil_node)
210
+ ) for organic_carbon_per_kg_soil_node, datestr in zip(organic_carbon_per_kg_soil_nodes, dates)
211
+ ]
212
+
213
+ return [_measurement(site, value, depth_upper, depth_lower, datestr) for value, datestr in zip(values, dates)]
214
+
215
+
216
+ # --- RESCALE `organicCarbonPerHa` ---
66
217
 
67
218
 
68
219
  def _c_to_depth(d: float) -> float:
@@ -72,7 +223,7 @@ def _c_to_depth(d: float) -> float:
72
223
  Parameters
73
224
  ----------
74
225
  d : float
75
- Measurement depth in meters (min `0`, max `1`).
226
+ Measurement depth in metres (min `0`, max `1`).
76
227
 
77
228
  Returns
78
229
  -------
@@ -84,20 +235,19 @@ def _c_to_depth(d: float) -> float:
84
235
 
85
236
  def _cdf(depth_upper: float, depth_lower: float) -> float:
86
237
  """
87
- The ratio between the carbon stock per m2 to depth `d` and the carbon
88
- stock per meter to depth `1`.
238
+ The ratio between the carbon stock per m2 to depth `d` and the carbon stock per m2 to depth `1`.
89
239
 
90
240
  Parameters
91
241
  ----------
92
242
  depth_upper : float
93
- Measurement depth upper in meters (min `0`, max `1`).
243
+ Measurement depth upper in metres (min `0`, max `1`).
94
244
  depth_lower : float
95
- Measurement depth lower in meters (min `0`, max `1`).
245
+ Measurement depth lower in metres (min `0`, max `1`).
96
246
 
97
247
  Returns
98
248
  -------
99
249
  float
100
- The proportion of carbon stored between `depth_upper` and `depth_lower` compared to between `0` and `1` meters.
250
+ The proportion of carbon stored between `depth_upper` and `depth_lower` compared to between `0` and `1` metres.
101
251
  """
102
252
  return (_c_to_depth(depth_lower) - _c_to_depth(depth_upper)) / _c_to_depth(1)
103
253
 
@@ -137,99 +287,111 @@ def _rescale_soc_value(
137
287
  return source_value * (cd_target / cd_measurement)
138
288
 
139
289
 
140
- def _get_last_date(dates: list[str]) -> str:
290
+ def _should_run_rescale_node(node: list) -> bool:
141
291
  """
142
- Reduces a node's dates field down to a single date. As the `arrayTreatment` for `organicCarbonPerKgSoil` and
143
- `organicCarbonPerHa` is `mean` the latest date should be selected.
292
+ Validate that a node has `depthUpper` = `0` and a `depthLower` < `100`.
144
293
  """
145
- return sorted(dates)[-1] if len(dates) > 0 else None
294
+ return all([
295
+ node.get('depthUpper', 1) == RESCALE_DEPTH_UPPER,
296
+ node.get('depthLower', 101) <= MAX_DEPTH_LOWER
297
+ ])
146
298
 
147
299
 
148
- def _run_rescale(site: dict, measurements: list):
149
- measurements = [m for m in measurements if all([
150
- m.get('depthUpper', 1) == RESCALE_DEPTH_UPPER,
151
- m.get('depthLower', 101) <= 100
152
- ])]
153
- # order measurements by depthLower and use the biggest
154
- measurement = sorted(measurements, key=lambda x: x.get('depthLower'))[-1] if len(measurements) > 0 else None
300
+ def _should_run_rescale_group(nodes: list) -> bool:
301
+ """
302
+ Validate that a list of nodes doesn't contain a node with `depthUpper` = `0` and a `depthLower` < `30`.
303
+ """
304
+ return not any([
305
+ node for node in nodes if all([
306
+ node.get('depthUpper', 1) == RESCALE_DEPTH_UPPER,
307
+ node.get('depthLower', 101) == RESCALE_DEPTH_LOWER
308
+ ])
309
+ ])
155
310
 
156
- value = _rescale_soc_value(
157
- measurement_value(measurement),
158
- RESCALE_DEPTH_UPPER, measurement.get('depthLower'),
159
- RESCALE_DEPTH_UPPER, RESCALE_DEPTH_LOWER,
160
- ) if measurement else None
161
311
 
162
- date = _get_last_date(measurement.get("dates", [])) if measurement else None
312
+ def _should_run_rescale(organic_carbon_per_ha_nodes: list) -> tuple[bool, dict[str, list[dict]]]:
313
+ """
314
+ Pre-process `organicCarbonPerHa` nodes and determine whether any need to be rescaled to a depth interval of 0-30cm.
315
+ """
316
+ grouped_nodes = group_nodes_by_last_date(
317
+ [node for node in organic_carbon_per_ha_nodes if _should_run_rescale_node(node)]
318
+ )
319
+
320
+ valid_grouped_nodes = {
321
+ datestr: nodes for datestr, nodes in grouped_nodes.items()
322
+ if _should_run_rescale_group(nodes)
323
+ }
163
324
 
164
- return [_measurement(site, value, RESCALE_DEPTH_UPPER, RESCALE_DEPTH_LOWER, date)] if value is not None else []
325
+ should_run = bool(valid_grouped_nodes)
165
326
 
327
+ logs = {
328
+ "should_run_rescale": should_run,
329
+ "inventory_rescale": log_as_table(
330
+ {
331
+ "date": str(datestr),
332
+ "should-run": datestr in valid_grouped_nodes.keys()
333
+ } for datestr, nodes in grouped_nodes.items()
334
+ ) or None
335
+ }
166
336
 
167
- def _run(site: dict, measurements: list):
168
- soilBulkDensity = measurement_value(find_term_match(measurements, 'soilBulkDensity'))
169
- organicCarbonPerKgSoil = find_term_match(measurements, 'organicCarbonPerKgSoil')
170
- organicCarbonPerKgSoil_value = measurement_value(organicCarbonPerKgSoil)
337
+ return should_run, logs, valid_grouped_nodes
171
338
 
172
- value = (
173
- organicCarbonPerKgSoil.get('depthLower') - organicCarbonPerKgSoil.get('depthUpper')
174
- ) * soilBulkDensity * (organicCarbonPerKgSoil_value/10) * 1000
175
339
 
176
- depthUpper = organicCarbonPerKgSoil.get('depthUpper')
177
- depthLower = organicCarbonPerKgSoil.get('depthLower')
178
- date = _get_last_date(organicCarbonPerKgSoil.get("dates", []))
340
+ def _depth_distance(node: dict):
341
+ return abs(node.get('depthLower', 101) - RESCALE_DEPTH_LOWER)
179
342
 
180
- return _measurement(site, value, depthUpper, depthLower, date)
181
343
 
344
+ def _get_most_relevant_soc_node(organic_carbon_per_ha_nodes: list[dict]):
345
+ """
346
+ Find the `organic_carbon_per_ha_node` with the closest depth interval to 0 - 30cm. `depthLowers` greater than 30cm
347
+ are prioritised. Returns `{}` if input list is empty.
348
+ """
349
+ priority_nodes = [
350
+ node for node in organic_carbon_per_ha_nodes
351
+ if 'depthLower' in node and node.get('depthLower') >= RESCALE_DEPTH_LOWER
352
+ ]
353
+ nodes = priority_nodes or organic_carbon_per_ha_nodes # If priority nodes are available use them.
182
354
 
183
- def _should_run_measurements(site: dict, measurements: list):
184
- soilBulkDensity = find_term_match(measurements, 'soilBulkDensity', None)
185
- has_soilBulkDensity_depthLower = (soilBulkDensity or {}).get('depthLower') is not None
186
- has_soilBulkDensity_depthUpper = (soilBulkDensity or {}).get('depthUpper') is not None
187
- organicCarbonPerKgSoil = find_term_match(measurements, 'organicCarbonPerKgSoil', None)
188
- has_organicCarbonPerKgSoil_depthLower = (organicCarbonPerKgSoil or {}).get('depthLower') is not None
189
- has_organicCarbonPerKgSoil_depthUpper = (organicCarbonPerKgSoil or {}).get('depthUpper') is not None
355
+ return next(node for node in nodes if _depth_distance(node) == min(_depth_distance(node) for node in nodes))
190
356
 
191
- depth_logs = {
192
- _group_measurement_key(measurements[0], include_dates=False): ';'.join([
193
- '_'.join([
194
- 'id:soilBulkDensity',
195
- f"hasDepthLower:{has_soilBulkDensity_depthLower}",
196
- f"hasDepthUpper:{has_soilBulkDensity_depthUpper}"
197
- ]),
198
- '_'.join([
199
- 'id:organicCarbonPerKgSoil',
200
- f"hasDepthLower:{has_organicCarbonPerKgSoil_depthLower}",
201
- f"hasDepthUpper:{has_organicCarbonPerKgSoil_depthUpper}"
202
- ])
203
- ])
204
- } if len(measurements) > 0 else {}
205
357
 
206
- logRequirements(site, model=MODEL, term=TERM_ID,
207
- **depth_logs)
358
+ def _run_rescale(site: dict, organic_carbon_per_ha_nodes: list[dict]) -> list[dict]:
359
+ """
360
+ For each unique measurement date, rescale the deepest `organicCarbonPerHa` node to a depth of 0 to 30cm.
361
+ """
362
+ node = _get_most_relevant_soc_node(organic_carbon_per_ha_nodes)
208
363
 
209
- should_run = all([
210
- has_soilBulkDensity_depthLower, has_soilBulkDensity_depthUpper,
211
- has_organicCarbonPerKgSoil_depthLower, has_organicCarbonPerKgSoil_depthUpper
212
- ])
213
- return should_run
364
+ value = _rescale_soc_value(
365
+ measurement_value(node),
366
+ RESCALE_DEPTH_UPPER, node.get('depthLower'),
367
+ RESCALE_DEPTH_UPPER, RESCALE_DEPTH_LOWER,
368
+ ) if node else None
369
+ date = _get_last_date(node.get("dates", [])) if node else None
370
+
371
+ return _measurement(site, value, RESCALE_DEPTH_UPPER, RESCALE_DEPTH_LOWER, date) if value is not None else []
214
372
 
215
373
 
216
- def _should_run(site: dict):
217
- grouped_measurements = list(group_measurements_by_depth(site.get('measurements', [])).values())
218
- values = [(measurements, _should_run_measurements(site, measurements)) for measurements in grouped_measurements]
219
- should_run = any([_should_run for measurements, _should_run in values])
220
- logShouldRun(site, MODEL, TERM_ID, should_run)
221
- return should_run, [measurements for measurements, _should_run in values if _should_run]
374
+ # --- RUN MODEL ---
222
375
 
223
376
 
224
377
  def run(site: dict):
225
- should_run, values = _should_run(site)
226
- calculated_measurements = [_run(site, value) for value in values] if should_run else []
378
+ should_run_calculation, logs_calculation, grouped_measurements = _should_run_calculation(site)
379
+ result_calculation = (
380
+ flatten([_run_calculation(site, depth_key, nodes) for depth_key, nodes in grouped_measurements.items()])
381
+ if should_run_calculation else []
382
+ )
227
383
 
228
- # rescale from existing and added measurements matching Term
229
- all_measurements = [
230
- m for m in (site.get('measurements', []) + calculated_measurements)
231
- if m.get('term', {}).get('@id') == TERM_ID
232
- ]
233
- return calculated_measurements + (
234
- _run_rescale(site, all_measurements) if _should_run_rescale(all_measurements) else []
384
+ oc_per_ha_nodes = (
385
+ result_calculation + [m for m in site.get('measurements', []) if m.get('term', {}).get('@id') == TERM_ID]
235
386
  )
387
+
388
+ should_run_rescale, logs_rescale, grouped_oc_per_ha_nodes = _should_run_rescale(oc_per_ha_nodes)
389
+ result_rescale = (
390
+ [_run_rescale(site, nodes) for nodes in grouped_oc_per_ha_nodes.values()]
391
+ if should_run_rescale else []
392
+ )
393
+
394
+ logRequirements(site, model=MODEL, term=TERM_ID, **logs_calculation, **logs_rescale)
395
+ logShouldRun(site, MODEL, TERM_ID, should_run=should_run_calculation or should_run_rescale)
396
+
397
+ return result_calculation + result_rescale
@@ -52,9 +52,10 @@ def _emission(value: float):
52
52
  def _run(cycle: dict, content_list_of_items: list, N_total: float):
53
53
  n2OToAirSoilFlux = _get_value(cycle, content_list_of_items, N_total, TERM_ID)
54
54
  N_crop_residue = get_crop_residue_decomposition_N_total(cycle)
55
- return [_emission(N_crop_residue / N_total * n2OToAirSoilFlux if all([
55
+ value = N_crop_residue / N_total * n2OToAirSoilFlux if all([
56
56
  N_crop_residue, N_total
57
- ]) else 0)]
57
+ ]) else 0
58
+ return [_emission(value)]
58
59
 
59
60
 
60
61
  def run(cycle: dict):
@@ -59,9 +59,10 @@ def _emission(value: float):
59
59
  def _run(cycle: dict, content_list_of_items: list, N_total: float):
60
60
  n2OToAirSoilFlux = _get_value(cycle, content_list_of_items, N_total, TERM_ID)
61
61
  N_excreta = get_excreta_N_total(cycle)
62
- return [_emission(N_excreta / N_total * n2OToAirSoilFlux if all([
62
+ value = N_excreta / N_total * n2OToAirSoilFlux if all([
63
63
  N_excreta, N_total
64
- ]) else 0)]
64
+ ]) else 0
65
+ return [_emission(value)]
65
66
 
66
67
 
67
68
  def run(cycle: dict):
@@ -54,9 +54,10 @@ def _emission(value: float):
54
54
  def _run(cycle: dict, content_list_of_items: list, N_total: float):
55
55
  n2OToAirSoilFlux = _get_value(cycle, content_list_of_items, N_total, TERM_ID)
56
56
  N_iorganic_fertiliser = get_inorganic_fertiliser_N_total(cycle)
57
- return [_emission(N_iorganic_fertiliser / N_total * n2OToAirSoilFlux if all([
57
+ value = N_iorganic_fertiliser / N_total * n2OToAirSoilFlux if all([
58
58
  N_iorganic_fertiliser, N_total
59
- ]) else 0)]
59
+ ]) else 0
60
+ return [_emission(value)]
60
61
 
61
62
 
62
63
  def run(cycle: dict):
@@ -54,9 +54,10 @@ def _emission(value: float):
54
54
  def _run(cycle: dict, content_list_of_items: list, N_total: float):
55
55
  n2OToAirSoilFlux = _get_value(cycle, content_list_of_items, N_total, TERM_ID)
56
56
  N_organic_fertiliser = get_organic_fertiliser_N_total(cycle)
57
- return [_emission(N_organic_fertiliser / N_total * n2OToAirSoilFlux if all([
57
+ value = N_organic_fertiliser / N_total * n2OToAirSoilFlux if all([
58
58
  N_organic_fertiliser, N_total
59
- ]) else 0)]
59
+ ]) else 0
60
+ return [_emission(value)]
60
61
 
61
62
 
62
63
  def run(cycle: dict):
@@ -47,9 +47,10 @@ def _emission(value: float):
47
47
  def _run(cycle: dict, ecoClimateZone: str, nitrogenContent: float, N_total: float):
48
48
  noxToAirSoilFlux = _get_value(cycle, ecoClimateZone, nitrogenContent, N_total, TERM_ID)
49
49
  N_crop_residue = get_crop_residue_decomposition_N_total(cycle)
50
- return [_emission(N_crop_residue / N_total * noxToAirSoilFlux if all([
50
+ value = N_crop_residue / N_total * noxToAirSoilFlux if all([
51
51
  N_crop_residue, N_total
52
- ]) else 0)]
52
+ ]) else 0
53
+ return [_emission(value)]
53
54
 
54
55
 
55
56
  def run(cycle: dict):
@@ -54,9 +54,10 @@ def _emission(value: float):
54
54
  def _run(cycle: dict, ecoClimateZone: str, nitrogenContent: float, N_total: float):
55
55
  noxToAirSoilFlux = _get_value(cycle, ecoClimateZone, nitrogenContent, N_total, TERM_ID)
56
56
  N_excreta = get_excreta_N_total(cycle)
57
- return [_emission(N_excreta / N_total * noxToAirSoilFlux if all([
57
+ value = N_excreta / N_total * noxToAirSoilFlux if all([
58
58
  N_excreta, N_total
59
- ]) else 0)]
59
+ ]) else 0
60
+ return [_emission(value)]
60
61
 
61
62
 
62
63
  def run(cycle: dict):
@@ -49,9 +49,10 @@ def _emission(value: float):
49
49
  def _run(cycle: dict, ecoClimateZone: str, nitrogenContent: float, N_total: float):
50
50
  noxToAirSoilFlux = _get_value(cycle, ecoClimateZone, nitrogenContent, N_total, TERM_ID)
51
51
  N_inorganic_fertiliser = get_inorganic_fertiliser_N_total(cycle)
52
- return [_emission(N_inorganic_fertiliser / N_total * noxToAirSoilFlux if all([
52
+ value = N_inorganic_fertiliser / N_total * noxToAirSoilFlux if all([
53
53
  N_inorganic_fertiliser, N_total
54
- ]) else 0)]
54
+ ]) else 0
55
+ return [_emission(value)]
55
56
 
56
57
 
57
58
  def run(cycle: dict):
@@ -49,9 +49,10 @@ def _emission(value: float):
49
49
  def _run(cycle: dict, ecoClimateZone: str, nitrogenContent: float, N_total: float):
50
50
  noxToAirSoilFlux = _get_value(cycle, ecoClimateZone, nitrogenContent, N_total, TERM_ID)
51
51
  N_organic_fertiliser = get_organic_fertiliser_N_total(cycle)
52
- return [_emission(N_organic_fertiliser / N_total * noxToAirSoilFlux if all([
52
+ value = N_organic_fertiliser / N_total * noxToAirSoilFlux if all([
53
53
  N_organic_fertiliser, N_total
54
- ]) else 0)]
54
+ ]) else 0
55
+ return [_emission(value)]
55
56
 
56
57
 
57
58
  def run(cycle: dict):
@@ -44,9 +44,10 @@ def _emission(value: float):
44
44
  def _run(cycle: dict, country_id: str, N_total: float):
45
45
  noxToAirSoilFlux = _get_value(cycle, country_id, N_total, TERM_ID)
46
46
  N_crop_residue = get_crop_residue_decomposition_N_total(cycle)
47
- return [_emission(N_crop_residue / N_total * noxToAirSoilFlux if all([
47
+ value = N_crop_residue / N_total * noxToAirSoilFlux if all([
48
48
  N_crop_residue, N_total
49
- ]) else 0)]
49
+ ]) else 0
50
+ return [_emission(value)]
50
51
 
51
52
 
52
53
  def run(cycle: dict):
@@ -51,9 +51,10 @@ def _emission(value: float):
51
51
  def _run(cycle: dict, country_id: str, N_total: float):
52
52
  noxToAirSoilFlux = _get_value(cycle, country_id, N_total, TERM_ID)
53
53
  N_excreta = get_excreta_N_total(cycle)
54
- return [_emission(N_excreta / N_total * noxToAirSoilFlux if all([
54
+ value = N_excreta / N_total * noxToAirSoilFlux if all([
55
55
  N_excreta, N_total
56
- ]) else 0)]
56
+ ]) else 0
57
+ return [_emission(value)]
57
58
 
58
59
 
59
60
  def run(cycle: dict):
@@ -46,9 +46,10 @@ def _emission(value: float):
46
46
  def _run(cycle: dict, country_id: str, N_total: float):
47
47
  noxToAirSoilFlux = _get_value(cycle, country_id, N_total, TERM_ID)
48
48
  N_inorganic_fertiliser = get_inorganic_fertiliser_N_total(cycle)
49
- return [_emission(N_inorganic_fertiliser / N_total * noxToAirSoilFlux if all([
49
+ value = N_inorganic_fertiliser / N_total * noxToAirSoilFlux if all([
50
50
  N_inorganic_fertiliser, N_total
51
- ]) else 0)]
51
+ ]) else 0
52
+ return [_emission(value)]
52
53
 
53
54
 
54
55
  def run(cycle: dict):
@@ -46,9 +46,10 @@ def _emission(value: float):
46
46
  def _run(cycle: dict, country_id: str, N_total: float):
47
47
  noxToAirSoilFlux = _get_value(cycle, country_id, N_total, TERM_ID)
48
48
  N_organic_fertiliser = get_organic_fertiliser_N_total(cycle)
49
- return [_emission(N_organic_fertiliser / N_total * noxToAirSoilFlux if all([
49
+ value = N_organic_fertiliser / N_total * noxToAirSoilFlux if all([
50
50
  N_organic_fertiliser, N_total
51
- ]) else 0)]
51
+ ]) else 0
52
+ return [_emission(value)]
52
53
 
53
54
 
54
55
  def run(cycle: dict):
@@ -43,6 +43,7 @@ def find_closest_impact(cycle: dict, end_date: str, input: dict, region: dict, c
43
43
  'must': non_empty_list([
44
44
  {'match': {'@type': SchemaType.IMPACTASSESSMENT.value}},
45
45
  {'match': {'aggregated': 'true'}},
46
+ {'match': {'aggregatedDataValidated': 'true'}},
46
47
  {
47
48
  'bool': {
48
49
  # handle old ImpactAssessment data