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.
- hestia_earth/models/config/Cycle.json +23 -0
- hestia_earth/models/config/ImpactAssessment.json +1 -1
- hestia_earth/models/cycle/aboveGroundCropResidueTotal.py +50 -0
- hestia_earth/models/cycle/completeness/freshForage.py +2 -2
- hestia_earth/models/emepEea2019/co2ToAirFuelCombustion.py +2 -27
- hestia_earth/models/emepEea2019/fuelCombustion_utils.py +107 -0
- hestia_earth/models/emepEea2019/n2OToAirFuelCombustionDirect.py +2 -27
- hestia_earth/models/emepEea2019/nh3ToAirFuelCombustion.py +33 -0
- hestia_earth/models/emepEea2019/noxToAirFuelCombustion.py +2 -27
- hestia_earth/models/emepEea2019/so2ToAirFuelCombustion.py +2 -27
- hestia_earth/models/emepEea2019/utils.py +1 -73
- hestia_earth/models/hestia/landCover.py +6 -5
- hestia_earth/models/hestia/landOccupationDuringCycle.py +264 -0
- hestia_earth/models/hestia/management.py +109 -68
- hestia_earth/models/hestia/utils.py +0 -1
- hestia_earth/models/ipcc2006/aboveGroundCropResidueTotal.py +20 -11
- hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +37 -28
- hestia_earth/models/ipcc2019/animal/pastureGrass.py +15 -8
- hestia_earth/models/ipcc2019/animal/utils.py +2 -2
- hestia_earth/models/ipcc2019/pastureGrass.py +7 -2
- hestia_earth/models/mocking/search-results.json +1566 -1562
- hestia_earth/models/utils/cropResidue.py +5 -0
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/METADATA +1 -1
- {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/RECORD +40 -33
- tests/models/cycle/test_aboveGroundCropResidueTotal.py +20 -0
- tests/models/emepEea2019/test_co2ToAirFuelCombustion.py +2 -1
- tests/models/emepEea2019/test_n2OToAirFuelCombustionDirect.py +2 -1
- tests/models/emepEea2019/test_nh3ToAirFuelCombustion.py +34 -0
- tests/models/emepEea2019/test_noxToAirFuelCombustion.py +2 -1
- tests/models/emepEea2019/test_so2ToAirFuelCombustion.py +2 -1
- tests/models/environmentalFootprintV3_1/test_scarcityWeightedWaterUse.py +0 -1
- tests/models/hestia/test_landCover.py +1 -1
- tests/models/hestia/test_landOccupationDuringCycle.py +68 -0
- tests/models/hestia/test_management.py +29 -1
- tests/models/ipcc2006/test_aboveGroundCropResidueTotal.py +9 -6
- tests/models/ipcc2019/test_aboveGroundCropResidueTotal.py +16 -42
- {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.74.1.dist-info → hestia_earth_models-0.74.3.dist-info}/WHEEL +0 -0
- {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,
|
|
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": "
|
|
73
|
-
"organicFertiliser": "
|
|
74
|
-
"soilAmendment": "
|
|
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
|
|
231
|
-
|
|
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
|
-
|
|
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(
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
}
|
|
248
|
-
for input in
|
|
249
|
-
if
|
|
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(
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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,
|
|
99
|
+
return should_run, value
|
|
91
100
|
|
|
92
101
|
|
|
93
102
|
def run(cycle: dict):
|
|
94
|
-
should_run,
|
|
95
|
-
return
|
|
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
|
|
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'
|
|
48
|
-
yield_dm = get_yield_dm(TERM_ID, term)
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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,
|
|
97
|
+
return should_run, value
|
|
89
98
|
|
|
90
99
|
|
|
91
100
|
def run(cycle: dict):
|
|
92
|
-
should_run,
|
|
93
|
-
return
|
|
101
|
+
should_run, value = _should_run(cycle)
|
|
102
|
+
return [_product(value)] if should_run else []
|