hestia-earth-models 0.74.1__py3-none-any.whl → 0.74.3__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 (40) hide show
  1. hestia_earth/models/config/Cycle.json +23 -0
  2. hestia_earth/models/config/ImpactAssessment.json +1 -1
  3. hestia_earth/models/cycle/aboveGroundCropResidueTotal.py +50 -0
  4. hestia_earth/models/cycle/completeness/freshForage.py +2 -2
  5. hestia_earth/models/emepEea2019/co2ToAirFuelCombustion.py +2 -27
  6. hestia_earth/models/emepEea2019/fuelCombustion_utils.py +107 -0
  7. hestia_earth/models/emepEea2019/n2OToAirFuelCombustionDirect.py +2 -27
  8. hestia_earth/models/emepEea2019/nh3ToAirFuelCombustion.py +33 -0
  9. hestia_earth/models/emepEea2019/noxToAirFuelCombustion.py +2 -27
  10. hestia_earth/models/emepEea2019/so2ToAirFuelCombustion.py +2 -27
  11. hestia_earth/models/emepEea2019/utils.py +1 -73
  12. hestia_earth/models/hestia/landCover.py +6 -5
  13. hestia_earth/models/hestia/landOccupationDuringCycle.py +264 -0
  14. hestia_earth/models/hestia/management.py +109 -68
  15. hestia_earth/models/hestia/utils.py +0 -1
  16. hestia_earth/models/ipcc2006/aboveGroundCropResidueTotal.py +20 -11
  17. hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +37 -28
  18. hestia_earth/models/ipcc2019/animal/pastureGrass.py +15 -8
  19. hestia_earth/models/ipcc2019/animal/utils.py +2 -2
  20. hestia_earth/models/ipcc2019/pastureGrass.py +7 -2
  21. hestia_earth/models/mocking/search-results.json +1566 -1562
  22. hestia_earth/models/utils/cropResidue.py +5 -0
  23. hestia_earth/models/version.py +1 -1
  24. {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/METADATA +1 -1
  25. {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/RECORD +40 -33
  26. tests/models/cycle/test_aboveGroundCropResidueTotal.py +20 -0
  27. tests/models/emepEea2019/test_co2ToAirFuelCombustion.py +2 -1
  28. tests/models/emepEea2019/test_n2OToAirFuelCombustionDirect.py +2 -1
  29. tests/models/emepEea2019/test_nh3ToAirFuelCombustion.py +34 -0
  30. tests/models/emepEea2019/test_noxToAirFuelCombustion.py +2 -1
  31. tests/models/emepEea2019/test_so2ToAirFuelCombustion.py +2 -1
  32. tests/models/environmentalFootprintV3_1/test_scarcityWeightedWaterUse.py +0 -1
  33. tests/models/hestia/test_landCover.py +1 -1
  34. tests/models/hestia/test_landOccupationDuringCycle.py +68 -0
  35. tests/models/hestia/test_management.py +29 -1
  36. tests/models/ipcc2006/test_aboveGroundCropResidueTotal.py +9 -6
  37. tests/models/ipcc2019/test_aboveGroundCropResidueTotal.py +16 -42
  38. {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/LICENSE +0 -0
  39. {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/WHEEL +0 -0
  40. {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,264 @@
1
+ from functools import reduce
2
+ from typing import NamedTuple
3
+
4
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
5
+
6
+ from hestia_earth.models.utils import hectar_to_square_meter
7
+ from hestia_earth.models.utils.constant import DAYS_IN_YEAR
8
+ from hestia_earth.models.utils.indicator import _new_indicator
9
+ from hestia_earth.models.utils.impact_assessment import get_product
10
+ from hestia_earth.models.utils.site import get_land_cover_term_id as get_landCover_term_id_from_site_type
11
+ from hestia_earth.models.utils.crop import get_landCover_term_id
12
+ from hestia_earth.schema import CycleFunctionalUnit
13
+
14
+ from . import MODEL
15
+
16
+ REQUIREMENTS = {
17
+ "ImpactAssessment": {
18
+ "product": {
19
+ "@type": "Term",
20
+ "value": "> 0",
21
+ "optional": {
22
+ "@doc": "if the [cycle.functionalUnit](https://hestia.earth/schema/Cycle#functionalUnit) = 1 ha, additional properties are required", # noqa: E501
23
+ "economicValueShare": ">= 0"
24
+ }
25
+ },
26
+ "cycle": {
27
+ "@type": "Cycle",
28
+ "site": {
29
+ "@type": "Site",
30
+ "country": {"@type": "Term", "termType": "region"}
31
+ },
32
+ "siteArea": "",
33
+ "siteDuration": "",
34
+ "siteUnusedDuration": "",
35
+ "optional": {
36
+ "@doc": "When `otherSites` are provided, `otherSitesArea`, `otherSitesDuration` and `otherSitesUnusedDuration` are required", # noqa: E501
37
+ "otherSites": [{
38
+ "@type": "Site",
39
+ "country": {"@type": "Term", "termType": "region"}
40
+ }],
41
+ "otherSitesArea": "",
42
+ "otherSitesDuration": "",
43
+ "otherSitesUnusedDuration": ""
44
+ }
45
+ }
46
+ }
47
+ }
48
+ RETURNS = {
49
+ "Indicator": [{
50
+ "value": "",
51
+ "landCover": ""
52
+ }]
53
+ }
54
+ TERM_ID = 'landOccupationDuringCycle'
55
+
56
+
57
+ class SiteData(NamedTuple):
58
+ id: str # site.@id
59
+ area: float
60
+ duration: float
61
+ unused_duration: float
62
+ land_cover_id: str
63
+ country_id: str
64
+
65
+
66
+ def _indicator(term_id: str, value: float, land_cover_id: str, country_id: str):
67
+ indicator = _new_indicator(
68
+ term_id, model=MODEL, land_cover_id=land_cover_id, country_id=country_id
69
+ )
70
+ indicator['value'] = round(value, 6)
71
+ return indicator
72
+
73
+
74
+ def _calc_land_occupation_m2_per_ha(
75
+ site_area: float, site_duration: float, site_unused_duration: float
76
+ ) -> float:
77
+ """
78
+ Parameters
79
+ ----------
80
+ site_area : float
81
+ Area of the site in hectares.
82
+ site_duration : float
83
+ Site duration in days.
84
+ site_unused_duration : float
85
+ Site unused duration in days.
86
+
87
+ Returns
88
+ -------
89
+ float
90
+ """
91
+ return hectar_to_square_meter(site_area) * (site_duration + site_unused_duration) / DAYS_IN_YEAR
92
+
93
+
94
+ def _calc_land_occupation_m2_per_kg(
95
+ yield_: float, economic_value_share: float, land_occupation_m2_per_ha: float,
96
+ ) -> float:
97
+ """
98
+ Parameters
99
+ ----------
100
+ yield_ : float
101
+ Product yield in product units.
102
+ economic_value_share : float
103
+ Economic value share of the product in % (0-100).
104
+ land_occupation_m2_per_ha : float
105
+ Land occupation in m2 ha-1.
106
+
107
+ Returns
108
+ -------
109
+ float
110
+ """
111
+ return land_occupation_m2_per_ha * economic_value_share * 0.01 / yield_
112
+
113
+
114
+ _CYCLE_KEYS = (
115
+ "site", "siteArea", "siteDuration", "siteUnusedDuration"
116
+ )
117
+
118
+ _CYCLE_KEY_MAPPING = {
119
+ field: field.replace("site", "otherSites", 1) for field in _CYCLE_KEYS
120
+ }
121
+
122
+
123
+ def _build_inventory(cycle: dict, product: dict):
124
+ product_land_cover_id = get_landCover_term_id(product.get("term", {}), skip_debug=True)
125
+
126
+ cycle_data = {
127
+ key: [value] + cycle.get(otherSites_key, []) for key, otherSites_key in _CYCLE_KEY_MAPPING.items()
128
+ if (value := cycle.get(key))
129
+ }
130
+
131
+ n_sites = len(cycle_data.get("site", []))
132
+ should_build_inventory = n_sites > 0 and all([
133
+ len(cycle_data.get(key, [])) == n_sites for key in _CYCLE_KEYS[1:]
134
+ ])
135
+
136
+ inventory = [
137
+ SiteData(
138
+ id=site.get("@id"),
139
+ area=cycle_data["siteArea"][i],
140
+ duration=cycle_data["siteDuration"][i],
141
+ unused_duration=cycle_data["siteUnusedDuration"][i],
142
+ country_id=site.get("country", {}).get("@id"),
143
+ land_cover_id=product_land_cover_id or get_landCover_term_id_from_site_type(site.get("siteType", {}))
144
+ ) for i, site in enumerate(cycle_data.get("site", []))
145
+ ] if should_build_inventory else []
146
+
147
+ logs = {
148
+ "n_sites": n_sites
149
+ }
150
+
151
+ return inventory, logs
152
+
153
+
154
+ def _should_run_site_data(site_data: SiteData) -> bool:
155
+ return all([
156
+ site_data.area >= 0,
157
+ site_data.duration >= 0,
158
+ site_data.unused_duration >= 0,
159
+ site_data.land_cover_id,
160
+ site_data.country_id
161
+ ])
162
+
163
+
164
+ def _format_float(value: float, unit: str = "") -> str:
165
+ return " ".join(
166
+ string for string in [f"{value:.3g}", unit] if string
167
+ ) if value else "None"
168
+
169
+
170
+ _INVALID_CHARS = {"_", ":", ",", "="}
171
+ _REPLACEMENT_CHAR = "-"
172
+
173
+
174
+ def _format_str(value: str) -> str:
175
+ """Format a string for logging in a table. Remove all characters used to render the table on the front end."""
176
+ return reduce(lambda x, char: x.replace(char, _REPLACEMENT_CHAR), _INVALID_CHARS, str(value))
177
+
178
+
179
+ def _format_inventory(inventory: list[SiteData]) -> str:
180
+ return log_as_table(
181
+ {
182
+ "@id": _format_str(site_data.id),
183
+ "site-area": _format_float(site_data.area, "ha"),
184
+ "site-duration": _format_float(site_data.duration, "days"),
185
+ "site-unused-duration": _format_float(site_data.unused_duration, "days"),
186
+ "land-cover-id": _format_str(site_data.land_cover_id),
187
+ "country-id": _format_str(site_data.country_id)
188
+ } for site_data in inventory
189
+ ) if inventory else "None"
190
+
191
+
192
+ def _should_run(impact_assessment: dict):
193
+
194
+ cycle = impact_assessment.get("cycle")
195
+ functional_unit = cycle.get("functionalUnit")
196
+
197
+ product = get_product(impact_assessment)
198
+ yield_ = sum(product.get("value", []))
199
+ economic_value_share = (
200
+ 100 if functional_unit == CycleFunctionalUnit.RELATIVE.value
201
+ else product.get("economicValueShare")
202
+ )
203
+
204
+ inventory, logs = _build_inventory(cycle, product)
205
+
206
+ valid_inventory = inventory and all(_should_run_site_data(site_data) for site_data in inventory)
207
+
208
+ logRequirements(
209
+ impact_assessment,
210
+ model=MODEL,
211
+ term=TERM_ID,
212
+ functional_unit=functional_unit,
213
+ yield_=_format_float(yield_, product.get("term", {}).get("units")),
214
+ economic_value_share=_format_float(economic_value_share, "pct"),
215
+ site_inventory=_format_inventory(inventory),
216
+ valid_inventory=valid_inventory,
217
+ **logs
218
+ )
219
+
220
+ should_run = all([
221
+ yield_ > 0,
222
+ (
223
+ economic_value_share is not None
224
+ and economic_value_share >= 0
225
+ ),
226
+ valid_inventory
227
+ ])
228
+
229
+ logShouldRun(impact_assessment, MODEL, TERM_ID, should_run)
230
+
231
+ return should_run, yield_, economic_value_share, inventory
232
+
233
+
234
+ def _run(
235
+ yield_: float,
236
+ economic_value_share: float,
237
+ inventory: list[SiteData]
238
+ ) -> list[dict]:
239
+
240
+ def calc_occupation_by_group(result: dict, site_data: SiteData):
241
+ """Calculate the land occupation of a site and sum it with matching landCover/country groups."""
242
+
243
+ land_occupation_m2_per_ha = _calc_land_occupation_m2_per_ha(
244
+ site_data.area, site_data.duration, site_data.unused_duration
245
+ )
246
+
247
+ land_occupation_m2_per_kg = _calc_land_occupation_m2_per_kg(
248
+ yield_, economic_value_share, land_occupation_m2_per_ha
249
+ )
250
+
251
+ key = (site_data.land_cover_id, site_data.country_id)
252
+ return result | {key: result.get(key, 0) + land_occupation_m2_per_kg}
253
+
254
+ land_occupation_by_group = reduce(calc_occupation_by_group, inventory, {})
255
+
256
+ return [
257
+ _indicator(TERM_ID, value, land_cover_id, country_id)
258
+ for (land_cover_id, country_id), value in land_occupation_by_group.items()
259
+ ]
260
+
261
+
262
+ def run(impact_assessment: dict):
263
+ should_run, yield_, economic_value_share, inventory = _should_run(impact_assessment)
264
+ return _run(yield_, economic_value_share, inventory) if should_run else []
@@ -1,9 +1,9 @@
1
1
  from typing import List
2
2
  from datetime import timedelta, datetime
3
- from hestia_earth.schema import SchemaType, TermTermType, SiteSiteType, COMPLETENESS_MAPPING
3
+ from hestia_earth.schema import SchemaType, TermTermType, COMPLETENESS_MAPPING
4
4
  from hestia_earth.utils.lookup import column_name, get_table_value, download_lookup
5
5
  from hestia_earth.utils.model import filter_list_term_type
6
- from hestia_earth.utils.tools import safe_parse_float, flatten
6
+ from hestia_earth.utils.tools import safe_parse_float, flatten, is_number, is_boolean
7
7
  from hestia_earth.utils.blank_node import get_node_value
8
8
 
9
9
  from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
@@ -16,6 +16,7 @@ from hestia_earth.models.utils.site import (
16
16
  related_cycles, get_land_cover_term_id as get_landCover_term_id_from_site_type
17
17
  )
18
18
  from . import MODEL
19
+ from ..utils.property import get_property_lookup_value
19
20
 
20
21
  REQUIREMENTS = {
21
22
  "Site": {
@@ -67,13 +68,15 @@ RETURNS = {
67
68
  }]
68
69
  }
69
70
  LOOKUPS = {
71
+ "biochar": "inputGapFillManagementTermId",
70
72
  "crop": ["landCoverTermId", "maximumCycleDuration"],
71
73
  "forage": ["landCoverTermId"],
72
- "inorganicFertiliser": "nitrogenContent",
73
- "organicFertiliser": "ANIMAL_MANURE",
74
- "soilAmendment": "PRACTICE_INCREASING_C_INPUT",
74
+ "inorganicFertiliser": "inputGapFillManagementTermId",
75
+ "organicFertiliser": "inputGapFillManagementTermId",
76
+ "soilAmendment": "inputGapFillManagementTermId",
75
77
  "landUseManagement": "GAP_FILL_TO_MANAGEMENT",
76
- "property": "GAP_FILL_TO_MANAGEMENT"
78
+ "property": "GAP_FILL_TO_MANAGEMENT",
79
+ "landCover": "sumIs100Group"
77
80
  }
78
81
  MODEL_KEY = 'management'
79
82
 
@@ -87,41 +90,6 @@ _PRACTICES_TERM_TYPES = [
87
90
  TermTermType.LANDCOVER
88
91
  ]
89
92
  _PRACTICES_COMPLETENESS_MAPPING = COMPLETENESS_MAPPING.get(SchemaType.PRACTICE.value, {})
90
- _ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
91
- _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
92
- _ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserUsed"
93
- _AMENDMENT_INCREASING_C_USED_TERM_ID = "amendmentIncreasingSoilCarbonUsed"
94
- _INPUT_RULES = {
95
- TermTermType.INORGANICFERTILISER.value: (
96
- (
97
- TermTermType.INORGANICFERTILISER.value, # Lookup column
98
- lambda x: safe_parse_float(x, default=0) > 0, # Condition
99
- _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID # New term.
100
- ),
101
- ),
102
- TermTermType.SOILAMENDMENT.value: (
103
- (
104
- TermTermType.SOILAMENDMENT.value,
105
- lambda x: bool(x) is True,
106
- _AMENDMENT_INCREASING_C_USED_TERM_ID
107
- ),
108
- ),
109
- TermTermType.ORGANICFERTILISER.value: (
110
- (
111
- TermTermType.SOILAMENDMENT.value,
112
- lambda x: bool(x) is True,
113
- _ORGANIC_FERTILISER_USED_TERM_ID
114
- ),
115
- (
116
- TermTermType.ORGANICFERTILISER.value,
117
- lambda x: bool(x) is True,
118
- _ANIMAL_MANURE_USED_TERM_ID
119
- )
120
- )
121
- }
122
- _SKIP_LAND_COVER_SITE_TYPES = [
123
- SiteSiteType.CROPLAND.value
124
- ]
125
93
 
126
94
 
127
95
  def management(data: dict):
@@ -135,6 +103,10 @@ def management(data: dict):
135
103
  return node
136
104
 
137
105
 
106
+ def _is_cover_crop(term_id: str) -> bool:
107
+ return get_property_lookup_value(model=MODEL, term_id=term_id, column="blankNodesGroup") == "Cover crops"
108
+
109
+
138
110
  def _get_cycle_duration(cycle: dict, land_cover_id: str = None):
139
111
  cycle_duration = cycle.get('cycleDuration')
140
112
  lookup_value = None if cycle_duration or not land_cover_id else safe_parse_float(get_table_value(
@@ -227,58 +199,122 @@ def _get_relevant_items(cycle: dict, item_name: str, term_types: List[TermTermTy
227
199
  ]
228
200
 
229
201
 
230
- def _process_rule(node: dict, term: dict) -> list:
231
- term_types = []
232
- for column, condition, new_term in _INPUT_RULES[term.get('termType')]:
233
- lookup_result = get_lookup_value(term, LOOKUPS[column], model=MODEL, term=term.get('@id'), model_key=MODEL_KEY)
202
+ def _input_gap_fill_term_id(input: dict):
203
+ return get_lookup_value(input.get('term'), 'inputGapFillManagementTermId')
234
204
 
235
- if condition(lookup_result):
236
- term_types.append(node | {'id': new_term})
237
205
 
238
- return term_types
206
+ def _input_value_valid(input: dict):
207
+ value = get_node_value(input)
208
+ return value > 0 if is_number(value) else bool(value) is True if is_boolean(value) else False
239
209
 
240
210
 
241
- def _run_from_inputs(site: dict, cycle: dict) -> list:
242
- inputs = flatten([
243
- _process_rule(node={
211
+ def _run_from_inputs(cycle: dict) -> list:
212
+ inputs_with_ids = [
213
+ {
214
+ 'input-id': input.get('term', {}).get('@id'),
215
+ 'input-valid': _input_value_valid(input),
216
+ 'term-id': _input_gap_fill_term_id(input)
217
+ } for input in cycle.get('inputs', [])
218
+ ]
219
+ return [
220
+ {
221
+ 'id': input.get('term-id'),
244
222
  'value': True,
245
223
  'startDate': cycle.get('startDate'),
246
224
  'endDate': cycle.get('endDate')
247
- }, term=input.get('term'))
248
- for input in cycle.get('inputs', [])
249
- if input.get('term', {}).get('termType') in _INPUT_RULES
225
+ }
226
+ for input in inputs_with_ids
227
+ if all([
228
+ input.get('term-id'),
229
+ input.get('input-valid')
230
+ ])
231
+ ]
232
+
233
+
234
+ def _cycle_has_existing_non_cover_land_cover_nodes(cycle: dict) -> bool:
235
+ # if there are any landCover blank nodes in Practices without a Property from the
236
+ # blankNodesGroup = Cover crops lookup, return True, else False
237
+ return any([
238
+ practice for practice in cycle.get("practices", [])
239
+ if practice.get("term", {}).get("termType") == TermTermType.LANDCOVER.value
240
+ and not any(prop for prop in practice.get("properties", [])
241
+ if _is_cover_crop(prop.get("term", {}).get("@id")))
250
242
  ])
251
- return inputs
252
243
 
253
244
 
254
- def _run_from_siteType(site: dict, cycle: dict):
255
- site_type = site.get('siteType')
256
- site_type_id = get_landCover_term_id_from_site_type(site_type) if site_type not in _SKIP_LAND_COVER_SITE_TYPES \
257
- else None
245
+ def _run_from_siteType(cycle: dict, site_type_id: str):
258
246
  start_date = cycle.get('startDate') or _gap_filled_start_date(
259
247
  cycle=cycle,
260
248
  end_date=cycle.get('endDate'),
261
249
  land_cover_id=site_type_id
262
250
  ).get('startDate')
251
+ no_land_cover_blank_node = not _cycle_has_existing_non_cover_land_cover_nodes(cycle)
263
252
 
264
- should_run = all([site_type_id, start_date])
253
+ should_run = all([site_type_id, start_date, no_land_cover_blank_node])
265
254
  return [{
266
255
  'id': site_type_id,
256
+ 'termType': TermTermType.LANDCOVER.value,
267
257
  'value': 100,
268
258
  'startDate': start_date,
269
259
  'endDate': cycle.get('endDate')
270
260
  }] if should_run else []
271
261
 
272
262
 
273
- def _should_run_practice(practice: dict):
263
+ def _node_with_gap_filled_dates(node: dict, cycle: dict, site_type_id: str) -> dict:
264
+ return node | {
265
+ "endDate": node.get("endDate") or cycle.get("endDate"),
266
+ "startDate": node.get("startDate") or cycle.get('startDate') or _gap_filled_start_date(
267
+ cycle=cycle,
268
+ end_date=cycle.get('endDate'),
269
+ land_cover_id=site_type_id
270
+ ).get("startDate"),
271
+ }
272
+
273
+
274
+ def _dates_overlap(target_practice: dict, node: dict, cycle: dict, site_type_id: str) -> bool:
275
+ target_practice = _node_with_gap_filled_dates(node=target_practice, cycle=cycle, site_type_id=site_type_id)
276
+ node = _node_with_gap_filled_dates(node=node, cycle=cycle, site_type_id=site_type_id)
277
+ return all([
278
+ node["startDate"],
279
+ node["endDate"],
280
+ target_practice["startDate"],
281
+ target_practice["endDate"],
282
+ (
283
+ node["startDate"] <= target_practice["startDate"] <= node["endDate"] or
284
+ node["startDate"] < target_practice["endDate"] <= node["endDate"]
285
+ )
286
+ ])
287
+
288
+
289
+ def _should_run_practice(management_nodes: list, cycle: dict, site_type_id: str):
274
290
  """
275
291
  Include only landUseManagement practices where GAP_FILL_TO_MANAGEMENT = True
276
292
  """
277
- term = practice.get('term', {})
278
- return term.get('termType') != TermTermType.LANDUSEMANAGEMENT.value or _should_gap_fill(term)
293
+ landCover_management_nodes = [
294
+ _node_with_gap_filled_dates(node=node, cycle=cycle, site_type_id=site_type_id) | {
295
+ 'sumIs100Group': get_lookup_value(node.get("term", {}), 'sumIs100Group', skip_debug=True, model=MODEL)
296
+ }
297
+ for node in filter_list_term_type(management_nodes, TermTermType.LANDCOVER)
298
+ ]
279
299
 
300
+ def run(practice: dict):
301
+ term = practice.get('term', {})
302
+ target_group = get_lookup_value(practice.get("term", {}), 'sumIs100Group', skip_debug=True, model=MODEL)
303
+ has_other_land_cover_in_same_group = next((
304
+ True for node in landCover_management_nodes
305
+ if (
306
+ node['sumIs100Group'] == target_group and
307
+ _dates_overlap(target_practice=practice, node=node, cycle=cycle, site_type_id=site_type_id)
308
+ )
309
+ ), None) is not None
310
+ return (
311
+ (term.get('termType') != TermTermType.LANDUSEMANAGEMENT.value or _should_gap_fill(term)) and
312
+ not has_other_land_cover_in_same_group
313
+ )
314
+ return run
280
315
 
281
- def _run_from_practices(cycle: dict):
316
+
317
+ def _run_from_practices(site: dict, cycle: dict, site_type_id: str):
282
318
  practices = [
283
319
  _extract_node_value(
284
320
  _include_with_date_gap_fill(
@@ -292,13 +328,18 @@ def _run_from_practices(cycle: dict):
292
328
  completeness_mapping=_PRACTICES_COMPLETENESS_MAPPING
293
329
  )
294
330
  ]
295
- return list(map(_map_to_value, filter(_should_run_practice, practices)))
331
+ management_nodes = site.get("management", [])
332
+ return list(map(_map_to_value, filter(
333
+ _should_run_practice(management_nodes=management_nodes, cycle=cycle, site_type_id=site_type_id), practices
334
+ )))
296
335
 
297
336
 
298
337
  def _run_cycle(site: dict, cycle: dict):
299
- inputs = _run_from_inputs(site, cycle)
300
- site_types = _run_from_siteType(site=site, cycle=cycle)
301
- practices = _run_from_practices(cycle)
338
+ inputs = _run_from_inputs(cycle)
339
+ site_type = site.get('siteType')
340
+ site_type_id = get_landCover_term_id_from_site_type(site_type)
341
+ site_types = _run_from_siteType(cycle=cycle, site_type_id=site_type_id)
342
+ practices = _run_from_practices(site=site, cycle=cycle, site_type_id=site_type_id)
302
343
  return [
303
344
  node | {'cycle-id': cycle.get('@id')}
304
345
  for node in inputs + site_types + practices
@@ -35,7 +35,6 @@ LAND_USE_TERMS_FOR_TRANSFORMATION = {
35
35
  PERMANENT_PASTURE: ("permanentPasture", "Permanent pasture"),
36
36
  OTHER_LAND: ("otherLand", OTHER_LAND) # Not used yet
37
37
  }
38
- LAND_USE_NAMES_FROM_ID = {v[0]: k for k, v in LAND_USE_TERMS_FOR_TRANSFORMATION.items()} | {"cropland": TOTAL_CROPLAND}
39
38
 
40
39
 
41
40
  def crop_ipcc_land_use_category(
@@ -7,6 +7,7 @@ from hestia_earth.models.utils.property import get_node_property
7
7
  from hestia_earth.models.utils.completeness import _is_term_type_incomplete
8
8
  from hestia_earth.models.utils.product import _new_product
9
9
  from hestia_earth.models.utils.crop import get_crop_lookup_value
10
+ from hestia_earth.models.utils.cropResidue import sum_above_ground_crop_residue
10
11
  from . import MODEL
11
12
 
12
13
  REQUIREMENTS = {
@@ -58,11 +59,6 @@ def _get_value_dm(product: dict, dm_percent: float):
58
59
  ]) else (yield_dm * slope + intercept * 1000)
59
60
 
60
61
 
61
- def _run(product: dict, dm_property: dict):
62
- value = _get_value_dm(product, safe_parse_float(dm_property.get('value'), default=None))
63
- return [_product(value)] if value is not None else []
64
-
65
-
66
62
  def _should_run_product(product: dict):
67
63
  term_id = product.get('term', {}).get('@id')
68
64
  value = list_sum(product.get('value', [0]))
@@ -79,17 +75,30 @@ def _should_run(cycle: dict):
79
75
  dm_property = get_node_property(products[0], PROPERTY_KEY) if single_crop_product else {}
80
76
  term_type_incomplete = _is_term_type_incomplete(cycle, TERM_ID)
81
77
 
78
+ dm_value = safe_parse_float(dm_property.get('value'), default=None)
79
+ value = _get_value_dm(products[0], dm_value) if single_crop_product else None
80
+
81
+ above_ground_crop_residue = sum_above_ground_crop_residue(cycle)
82
+ is_value_below_sum_above_ground_crop_residue = not not value and value >= above_ground_crop_residue
83
+
82
84
  logRequirements(cycle, model=MODEL, term=TERM_ID,
83
85
  single_crop_product=single_crop_product,
84
86
  nb_products=len(products),
85
87
  dryMatter=dm_property.get('value'),
86
- term_type_cropResidue_incomplete=term_type_incomplete)
87
-
88
- should_run = all([term_type_incomplete, single_crop_product, dm_property])
88
+ term_type_cropResidue_incomplete=term_type_incomplete,
89
+ value=value,
90
+ is_value_below_sum_above_ground_crop_residue=is_value_below_sum_above_ground_crop_residue)
91
+
92
+ should_run = all([
93
+ term_type_incomplete,
94
+ single_crop_product,
95
+ value is not None,
96
+ is_value_below_sum_above_ground_crop_residue
97
+ ])
89
98
  logShouldRun(cycle, MODEL, TERM_ID, should_run)
90
- return should_run, products, dm_property
99
+ return should_run, value
91
100
 
92
101
 
93
102
  def run(cycle: dict):
94
- should_run, products, dm_property = _should_run(cycle)
95
- return _run(products[0], dm_property) if should_run else []
103
+ should_run, value = _should_run(cycle)
104
+ return [_product(value)] if should_run else []
@@ -2,10 +2,11 @@ from hestia_earth.schema import TermTermType
2
2
  from hestia_earth.utils.model import filter_list_term_type
3
3
  from hestia_earth.utils.tools import list_sum
4
4
 
5
- from hestia_earth.models.log import debugValues, logRequirements, logShouldRun, log_as_table
5
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
6
6
  from hestia_earth.models.utils.completeness import _is_term_type_incomplete
7
7
  from hestia_earth.models.utils.product import _new_product
8
8
  from hestia_earth.models.utils.property import get_node_property
9
+ from hestia_earth.models.utils.cropResidue import sum_above_ground_crop_residue
9
10
  from .utils import get_yield_dm
10
11
  from . import MODEL
11
12
 
@@ -43,10 +44,14 @@ def _product(value: float):
43
44
  def _product_value(product: dict):
44
45
  term = product.get('term', {})
45
46
  term_id = product.get('term', {}).get('@id')
46
- value = list_sum(product.get('value'))
47
- dm = get_node_property(product, PROPERTY_KEY).get('value', 0)
48
- yield_dm = get_yield_dm(TERM_ID, term) or 0
49
- total = value * dm / 100 * yield_dm
47
+ value = list_sum(product.get('value'), default=None)
48
+ dm = get_node_property(product, PROPERTY_KEY).get('value')
49
+ yield_dm = get_yield_dm(TERM_ID, term)
50
+ total = value * dm / 100 * yield_dm if all([
51
+ value is not None,
52
+ dm is not None,
53
+ yield_dm is not None,
54
+ ]) else None
50
55
  return {
51
56
  'id': term_id,
52
57
  'value': value,
@@ -56,38 +61,42 @@ def _product_value(product: dict):
56
61
  }
57
62
 
58
63
 
59
- def _run(cycle: dict, products: list):
60
- values = list(map(_product_value, products))
61
- debugValues(cycle, model=MODEL, term=TERM_ID,
62
- details=log_as_table(values))
63
- value = sum([value.get('total', 0) for value in values])
64
- return [_product(value)]
65
-
66
-
67
- def _should_run_product(product: dict):
68
- term = product.get('term', {})
69
- value = list_sum(product.get('value', [0]))
70
- prop = get_node_property(product, PROPERTY_KEY).get('value')
71
- yield_dm = get_yield_dm(TERM_ID, term)
72
- return all([value > 0, prop, yield_dm is not None])
64
+ def _should_run_product(value: dict): return value.get('total') is not None
73
65
 
74
66
 
75
67
  def _should_run(cycle: dict):
68
+ term_type_incomplete = _is_term_type_incomplete(cycle, TERM_ID)
69
+
76
70
  # filter crop products with matching data in the lookup
77
71
  products = filter_list_term_type(cycle.get('products', []), [TermTermType.CROP, TermTermType.FORAGE])
78
- products = list(filter(_should_run_product, products))
79
- has_crop_forage_products = len(products) > 0
80
- term_type_incomplete = _is_term_type_incomplete(cycle, TERM_ID)
72
+ values = list(map(_product_value, products))
73
+ valid_values = list(filter(_should_run_product, values))
74
+
75
+ has_crop_forage_products = len(valid_values) > 0
76
+
77
+ value = list_sum([(value.get('total') or 0) for value in valid_values], default=None)
78
+
79
+ above_ground_crop_residue = sum_above_ground_crop_residue(cycle)
80
+ is_value_below_sum_above_ground_crop_residue = not not value and value >= above_ground_crop_residue
81
81
 
82
82
  logRequirements(cycle, model=MODEL, term=TERM_ID,
83
83
  has_crop_forage_products_with_dryMatter=has_crop_forage_products,
84
- term_type_cropResidue_incomplete=term_type_incomplete)
85
-
86
- should_run = all([term_type_incomplete, has_crop_forage_products])
84
+ term_type_cropResidue_incomplete=term_type_incomplete,
85
+ sum_above_ground_crop_residue=above_ground_crop_residue,
86
+ value=value,
87
+ is_value_below_sum_above_ground_crop_residue=is_value_below_sum_above_ground_crop_residue,
88
+ details=log_as_table(values))
89
+
90
+ should_run = all([
91
+ term_type_incomplete,
92
+ has_crop_forage_products,
93
+ value is not None,
94
+ is_value_below_sum_above_ground_crop_residue
95
+ ])
87
96
  logShouldRun(cycle, MODEL, TERM_ID, should_run)
88
- return should_run, products
97
+ return should_run, value
89
98
 
90
99
 
91
100
  def run(cycle: dict):
92
- should_run, products = _should_run(cycle)
93
- return _run(cycle, products) if should_run else []
101
+ should_run, value = _should_run(cycle)
102
+ return [_product(value)] if should_run else []