hestia-earth-models 0.59.1__py3-none-any.whl → 0.59.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/cycle/irrigatedTypeUnspecified.py +6 -7
- hestia_earth/models/impact_assessment/irrigated.py +4 -1
- hestia_earth/models/ipcc2006/n2OToAirCropResidueDecompositionDirect.py +83 -0
- hestia_earth/models/ipcc2006/n2OToAirCropResidueDecompositionIndirect.py +3 -3
- hestia_earth/models/ipcc2006/n2OToAirExcretaIndirect.py +3 -3
- hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserIndirect.py +5 -4
- hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserIndirect.py +5 -4
- hestia_earth/models/ipcc2006/utils.py +12 -16
- hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +8 -7
- hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionIndirect.py +100 -0
- hestia_earth/models/ipcc2019/n2OToAirExcretaIndirect.py +100 -0
- hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +54 -61
- hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +58 -66
- hestia_earth/models/ipcc2019/nh3ToAirInorganicFertiliser.py +112 -0
- hestia_earth/models/ipcc2019/nh3ToAirOrganicFertiliser.py +107 -0
- hestia_earth/models/ipcc2019/noxToAirInorganicFertiliser.py +112 -0
- hestia_earth/models/ipcc2019/noxToAirOrganicFertiliser.py +107 -0
- hestia_earth/models/ipcc2019/organicCarbonPerHa.py +67 -21
- hestia_earth/models/ipcc2019/utils.py +28 -16
- hestia_earth/models/site/soilMeasurement.py +197 -0
- hestia_earth/models/utils/cycle.py +7 -6
- hestia_earth/models/utils/emission.py +15 -0
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/METADATA +1 -1
- {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/RECORD +40 -24
- tests/models/ipcc2006/test_n2OToAirCropResidueDecompositionDirect.py +50 -0
- tests/models/ipcc2019/test_n2OToAirCropResidueDecompositionIndirect.py +71 -0
- tests/models/ipcc2019/test_n2OToAirExcretaIndirect.py +71 -0
- tests/models/ipcc2019/test_n2OToAirInorganicFertiliserIndirect.py +36 -13
- tests/models/ipcc2019/test_n2OToAirOrganicFertiliserIndirect.py +36 -13
- tests/models/ipcc2019/test_nh3ToAirInorganicFertiliser.py +47 -0
- tests/models/ipcc2019/test_nh3ToAirOrganicFertiliser.py +35 -0
- tests/models/ipcc2019/test_noxToAirInorganicFertiliser.py +47 -0
- tests/models/ipcc2019/test_noxToAirOrganicFertiliser.py +35 -0
- tests/models/ipcc2019/test_organicCarbonPerHa.py +51 -5
- tests/models/site/test_soilMeasurement.py +159 -0
- tests/models/utils/test_blank_node.py +5 -5
- {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition, TermTermType
|
|
2
|
+
from hestia_earth.utils.model import filter_list_term_type
|
|
3
|
+
from hestia_earth.utils.tools import list_sum
|
|
4
|
+
|
|
5
|
+
from hestia_earth.models.log import logRequirements, logShouldRun, debugValues, log_as_table
|
|
6
|
+
from hestia_earth.models.utils.blank_node import get_N_total
|
|
7
|
+
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
8
|
+
from hestia_earth.models.utils.completeness import _is_term_type_complete
|
|
9
|
+
from hestia_earth.models.utils.emission import _new_emission
|
|
10
|
+
from hestia_earth.models.utils.cycle import get_organic_fertiliser_N_total
|
|
11
|
+
from hestia_earth.models.utils.term import get_lookup_value
|
|
12
|
+
from . import MODEL
|
|
13
|
+
|
|
14
|
+
REQUIREMENTS = {
|
|
15
|
+
"Cycle": {
|
|
16
|
+
"completeness.fertiliser": "True",
|
|
17
|
+
"inputs": [
|
|
18
|
+
{
|
|
19
|
+
"@type": "Input",
|
|
20
|
+
"value": "",
|
|
21
|
+
"term.termType": "organicFertiliser",
|
|
22
|
+
"optional": {
|
|
23
|
+
"properties": [{"@type": "Property", "value": "", "term.@id": "nitrogenContent"}]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
RETURNS = {
|
|
30
|
+
"Emission": [{
|
|
31
|
+
"value": "",
|
|
32
|
+
"sd": "",
|
|
33
|
+
"min": "",
|
|
34
|
+
"max": "",
|
|
35
|
+
"methodTier": "tier 1",
|
|
36
|
+
"statsDefinition": "modelled",
|
|
37
|
+
"methodModelDescription": "Aggregated version"
|
|
38
|
+
}]
|
|
39
|
+
}
|
|
40
|
+
LOOKUPS = {
|
|
41
|
+
"organicFertiliser": ["IPCC_2019_FRACGASM_NOx-N", "IPCC_2019_FRACGASM_NOx-N-min", "IPCC_2019_FRACGASM_NOx-N-max"]
|
|
42
|
+
}
|
|
43
|
+
TERM_ID = 'noxToAirOrganicFertiliser'
|
|
44
|
+
TIER = EmissionMethodTier.TIER_1.value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _emission(value: float, min: float, max: float):
|
|
48
|
+
emission = _new_emission(TERM_ID, MODEL)
|
|
49
|
+
emission['value'] = [value]
|
|
50
|
+
emission['min'] = [min]
|
|
51
|
+
emission['max'] = [max]
|
|
52
|
+
emission['methodTier'] = TIER
|
|
53
|
+
emission['statsDefinition'] = EmissionStatsDefinition.MODELLED.value
|
|
54
|
+
emission['methodModelDescription'] = 'Aggregated version'
|
|
55
|
+
return emission
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _input_values(input: dict):
|
|
59
|
+
N_total = list_sum(get_N_total([input]))
|
|
60
|
+
return {
|
|
61
|
+
'id': input.get('term', {}).get('@id'),
|
|
62
|
+
'N': N_total,
|
|
63
|
+
'value': get_lookup_value(input.get('term', {}), LOOKUPS['organicFertiliser'][0]),
|
|
64
|
+
'min': get_lookup_value(input.get('term', {}), LOOKUPS['organicFertiliser'][1]),
|
|
65
|
+
'max': get_lookup_value(input.get('term', {}), LOOKUPS['organicFertiliser'][2])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _filter_input_values(values: list, key: str): return [value for value in values if value.get(key)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _run(cycle: dict):
|
|
73
|
+
inputs = filter_list_term_type(cycle.get('inputs', []), TermTermType.ORGANICFERTILISER)
|
|
74
|
+
input_values = list(map(_input_values, inputs))
|
|
75
|
+
|
|
76
|
+
debugValues(cycle, model=MODEL, term=TERM_ID,
|
|
77
|
+
input_values=log_as_table(input_values))
|
|
78
|
+
|
|
79
|
+
value = list_sum([
|
|
80
|
+
v.get('N', 0) * v.get('value', 0) for v in _filter_input_values(input_values, 'value')
|
|
81
|
+
]) * get_atomic_conversion(Units.KG_NOX, Units.TO_N)
|
|
82
|
+
|
|
83
|
+
min = list_sum([
|
|
84
|
+
v.get('N', 0) * v.get('min', 0) for v in _filter_input_values(input_values, 'min')
|
|
85
|
+
]) * get_atomic_conversion(Units.KG_NOX, Units.TO_N)
|
|
86
|
+
|
|
87
|
+
max = list_sum([
|
|
88
|
+
v.get('N', 0) * v.get('max', 0) for v in _filter_input_values(input_values, 'max')
|
|
89
|
+
]) * get_atomic_conversion(Units.KG_NOX, Units.TO_N)
|
|
90
|
+
|
|
91
|
+
return [_emission(value, min, max)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _should_run(cycle: dict):
|
|
95
|
+
N_organic_fertiliser = get_organic_fertiliser_N_total(cycle)
|
|
96
|
+
fertiliser_complete = _is_term_type_complete(cycle, 'fertiliser')
|
|
97
|
+
|
|
98
|
+
logRequirements(cycle, model=MODEL, term=TERM_ID,
|
|
99
|
+
N_organic_fertiliser=N_organic_fertiliser,
|
|
100
|
+
term_type_fertiliser_complete=fertiliser_complete)
|
|
101
|
+
|
|
102
|
+
should_run = all([N_organic_fertiliser is not None, fertiliser_complete])
|
|
103
|
+
logShouldRun(cycle, MODEL, TERM_ID, should_run)
|
|
104
|
+
return should_run
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run(cycle: dict): return _run(cycle) if _should_run(cycle) else []
|
|
@@ -18,12 +18,15 @@ from typing import (
|
|
|
18
18
|
from hestia_earth.schema import (
|
|
19
19
|
CycleFunctionalUnit,
|
|
20
20
|
MeasurementMethodClassification,
|
|
21
|
+
SchemaType,
|
|
21
22
|
SiteSiteType,
|
|
22
23
|
TermTermType,
|
|
23
24
|
)
|
|
25
|
+
from hestia_earth.utils.api import find_related
|
|
24
26
|
from hestia_earth.utils.model import find_term_match, filter_list_term_type
|
|
25
27
|
from hestia_earth.utils.tools import flatten, list_sum, non_empty_list
|
|
26
28
|
|
|
29
|
+
from hestia_earth.models.utils import _load_calculated_node
|
|
27
30
|
from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
|
|
28
31
|
from hestia_earth.models.utils.blank_node import (
|
|
29
32
|
cumulative_nodes_match,
|
|
@@ -42,7 +45,6 @@ from hestia_earth.models.utils.measurement import (
|
|
|
42
45
|
_new_measurement,
|
|
43
46
|
)
|
|
44
47
|
from hestia_earth.models.utils.property import get_node_property
|
|
45
|
-
from hestia_earth.models.utils.site import related_cycles
|
|
46
48
|
from hestia_earth.models.utils.term import (
|
|
47
49
|
get_cover_crop_property_terms,
|
|
48
50
|
get_crop_residue_incorporated_or_left_on_field_terms,
|
|
@@ -105,7 +107,14 @@ REQUIREMENTS = {
|
|
|
105
107
|
"value": "",
|
|
106
108
|
"startDate": "",
|
|
107
109
|
"endDate": "",
|
|
108
|
-
"term.@id": "
|
|
110
|
+
"term.@id": "organicFertiliserUsed"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"@type": "Management",
|
|
114
|
+
"value": "",
|
|
115
|
+
"startDate": "",
|
|
116
|
+
"endDate": "",
|
|
117
|
+
"term.@id": "amendmentIncreasingSoilCarbonUsed"
|
|
109
118
|
},
|
|
110
119
|
{"@type": "Management", "value": "", "startDate": "", "endDate": "", "term.@id": "shortBareFallow"}
|
|
111
120
|
]
|
|
@@ -182,7 +191,7 @@ CARBON_CONTENT_TERM_ID = "carbonContent"
|
|
|
182
191
|
NITROGEN_CONTENT_TERM_ID = "nitrogenContent"
|
|
183
192
|
LIGNIN_CONTENT_TERM_ID = "ligninContent"
|
|
184
193
|
|
|
185
|
-
|
|
194
|
+
CARBON_INPUT_PROPERTY_TERM_IDS = [
|
|
186
195
|
CARBON_CONTENT_TERM_ID,
|
|
187
196
|
NITROGEN_CONTENT_TERM_ID,
|
|
188
197
|
LIGNIN_CONTENT_TERM_ID
|
|
@@ -190,8 +199,7 @@ CROP_RESIDUE_PROPERTY_TERM_IDS = [
|
|
|
190
199
|
|
|
191
200
|
CARBON_SOURCE_TERM_TYPES = [
|
|
192
201
|
TermTermType.ORGANICFERTILISER.value,
|
|
193
|
-
TermTermType.SOILAMENDMENT.value
|
|
194
|
-
TermTermType.SEED.value
|
|
202
|
+
TermTermType.SOILAMENDMENT.value
|
|
195
203
|
]
|
|
196
204
|
|
|
197
205
|
MIN_RUN_IN_PERIOD = 5
|
|
@@ -237,7 +245,8 @@ IMPROVED_PASTURE_TERM_ID = "improvedPasture"
|
|
|
237
245
|
SHORT_BARE_FALLOW_TERM_ID = "shortBareFallow"
|
|
238
246
|
ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
|
|
239
247
|
INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
|
|
240
|
-
ORGANIC_FERTILISER_USED_TERM_ID = "
|
|
248
|
+
ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserUsed"
|
|
249
|
+
SOIL_AMENDMENT_USED_TERM_ID = "amendmentIncreasingSoilCarbonUsed"
|
|
241
250
|
|
|
242
251
|
CLAY_CONTENT_MAX = 8
|
|
243
252
|
SAND_CONTENT_MIN = 70
|
|
@@ -1834,17 +1843,12 @@ def _iterate_carbon_source(node: dict) -> Union[CarbonSource, None]:
|
|
|
1834
1843
|
CarbonSource | None
|
|
1835
1844
|
A `CarbonSource` named tuple if the node is a carbon source with the required properties, else `None`.
|
|
1836
1845
|
"""
|
|
1837
|
-
term = node.get("term", {})
|
|
1838
1846
|
mass = list_sum(node.get("value", []))
|
|
1839
1847
|
carbon_content, nitrogen_content, lignin_content = (
|
|
1840
|
-
get_node_property(node, term_id
|
|
1848
|
+
get_node_property(node, term_id).get("value", 0)/100 for term_id in CARBON_INPUT_PROPERTY_TERM_IDS
|
|
1841
1849
|
)
|
|
1842
1850
|
|
|
1843
1851
|
should_run = all([
|
|
1844
|
-
any([
|
|
1845
|
-
term.get("@id", None) in get_crop_residue_incorporated_or_left_on_field_terms(),
|
|
1846
|
-
term.get("termType") in CARBON_SOURCE_TERM_TYPES
|
|
1847
|
-
]),
|
|
1848
1852
|
mass > 0,
|
|
1849
1853
|
0 < carbon_content <= 1,
|
|
1850
1854
|
0 < nitrogen_content <= 1,
|
|
@@ -1878,7 +1882,13 @@ def _get_carbon_sources_from_cycles(cycles: dict) -> list[CarbonSource]:
|
|
|
1878
1882
|
[cycle.get("inputs", []) + cycle.get("products", []) for cycle in cycles]
|
|
1879
1883
|
))
|
|
1880
1884
|
|
|
1881
|
-
return non_empty_list([
|
|
1885
|
+
return non_empty_list([
|
|
1886
|
+
_iterate_carbon_source(node) for node in inputs_and_products
|
|
1887
|
+
if any([
|
|
1888
|
+
node.get("term", {}).get("@id") in get_crop_residue_incorporated_or_left_on_field_terms(),
|
|
1889
|
+
node.get("term", {}).get("termType") in CARBON_SOURCE_TERM_TYPES
|
|
1890
|
+
])
|
|
1891
|
+
])
|
|
1882
1892
|
|
|
1883
1893
|
|
|
1884
1894
|
# --- TIER 2 SOC MODEL ---
|
|
@@ -1925,6 +1935,8 @@ def _run_tier_2(
|
|
|
1925
1935
|
The length of the run-in period in years, must be greater than or equal to 1, default value: `5`.
|
|
1926
1936
|
run_with_irrigation : bool, optional
|
|
1927
1937
|
`True` if the model should run while taking into account irrigation, `False` if not.
|
|
1938
|
+
sand_content : float, optional
|
|
1939
|
+
A back-up sand content for if none are found in the inventory, decimal proportion, default value: `0.33`.
|
|
1928
1940
|
params : dict | None, optional
|
|
1929
1941
|
Overrides for the model parameters. If `None` only default parameters will be used.
|
|
1930
1942
|
|
|
@@ -1952,8 +1964,11 @@ def _run_tier_2(
|
|
|
1952
1964
|
)
|
|
1953
1965
|
|
|
1954
1966
|
sand_content = next(
|
|
1955
|
-
|
|
1956
|
-
|
|
1967
|
+
(
|
|
1968
|
+
group[_InventoryKey.SAND_CONTENT] for group in valid_inventory.values()
|
|
1969
|
+
if _InventoryKey.SAND_CONTENT in group
|
|
1970
|
+
),
|
|
1971
|
+
sand_content
|
|
1957
1972
|
)
|
|
1958
1973
|
|
|
1959
1974
|
# --- MERGE ANY USER-SET PARAMETERS WITH THE IPCC DEFAULTS ---
|
|
@@ -3294,7 +3309,7 @@ def _get_carbon_input_kwargs(
|
|
|
3294
3309
|
|
|
3295
3310
|
has_organic_fertiliser_or_soil_amendment_used = any(
|
|
3296
3311
|
get_node_value(node) for node in land_use_management_nodes
|
|
3297
|
-
if node_term_match(node, ORGANIC_FERTILISER_USED_TERM_ID)
|
|
3312
|
+
if node_term_match(node, [ORGANIC_FERTILISER_USED_TERM_ID, SOIL_AMENDMENT_USED_TERM_ID])
|
|
3298
3313
|
)
|
|
3299
3314
|
|
|
3300
3315
|
has_practice_increasing_c_input = cumulative_nodes_match(
|
|
@@ -3539,7 +3554,7 @@ def _should_run(site: dict) -> tuple[bool, dict]:
|
|
|
3539
3554
|
site_type = site.get("siteType", "")
|
|
3540
3555
|
management_nodes = site.get("management", [])
|
|
3541
3556
|
measurement_nodes = site.get("measurements", [])
|
|
3542
|
-
cycles =
|
|
3557
|
+
cycles = _calculated_cycles(site.get("@id"))
|
|
3543
3558
|
|
|
3544
3559
|
has_management = len(management_nodes) > 0
|
|
3545
3560
|
has_measurements = len(measurement_nodes) > 0
|
|
@@ -3596,6 +3611,26 @@ def _should_run(site: dict) -> tuple[bool, dict]:
|
|
|
3596
3611
|
return should_run_tier_1, should_run_tier_2, inventory, kwargs
|
|
3597
3612
|
|
|
3598
3613
|
|
|
3614
|
+
def _calculated_cycles(site_id: str):
|
|
3615
|
+
"""
|
|
3616
|
+
Get the list of `Cycle`s related to the `Site`. Gets the `recalculated` data if available, else `original`.
|
|
3617
|
+
|
|
3618
|
+
Parameters
|
|
3619
|
+
----------
|
|
3620
|
+
site_id : str
|
|
3621
|
+
The `@id` of the `Site`.
|
|
3622
|
+
|
|
3623
|
+
Returns
|
|
3624
|
+
-------
|
|
3625
|
+
list[dict]
|
|
3626
|
+
The related `Cycle`s as `dict`.
|
|
3627
|
+
"""
|
|
3628
|
+
nodes = find_related(SchemaType.SITE, site_id, SchemaType.CYCLE)
|
|
3629
|
+
return list(
|
|
3630
|
+
map(lambda node: _load_calculated_node(node, SchemaType.CYCLE), nodes or [])
|
|
3631
|
+
)
|
|
3632
|
+
|
|
3633
|
+
|
|
3599
3634
|
def _should_run_tier_1(
|
|
3600
3635
|
inventory: dict,
|
|
3601
3636
|
*,
|
|
@@ -3615,6 +3650,8 @@ def _should_run_tier_1(
|
|
|
3615
3650
|
|
|
3616
3651
|
def _should_run_tier_2(
|
|
3617
3652
|
inventory: dict,
|
|
3653
|
+
*,
|
|
3654
|
+
sand_content: float = None,
|
|
3618
3655
|
**_
|
|
3619
3656
|
) -> bool:
|
|
3620
3657
|
"""
|
|
@@ -3624,7 +3661,7 @@ def _should_run_tier_2(
|
|
|
3624
3661
|
return all([
|
|
3625
3662
|
len(valid_years) >= MIN_RUN_IN_PERIOD,
|
|
3626
3663
|
check_consecutive(valid_years),
|
|
3627
|
-
any(inventory.get(year).get(_InventoryKey.SAND_CONTENT) for year in valid_years)
|
|
3664
|
+
any(inventory.get(year).get(_InventoryKey.SAND_CONTENT) for year in valid_years) or sand_content
|
|
3628
3665
|
])
|
|
3629
3666
|
|
|
3630
3667
|
|
|
@@ -3663,7 +3700,7 @@ def _log_inventory(inventory: dict) -> str:
|
|
|
3663
3700
|
),
|
|
3664
3701
|
"irrigated-monthly": (
|
|
3665
3702
|
" ".join(str(val) for val in group.get(_InventoryKey.IRRIGATED_MONTHLY, []))
|
|
3666
|
-
if group.get(_InventoryKey.
|
|
3703
|
+
if group.get(_InventoryKey.IRRIGATED_MONTHLY) else None
|
|
3667
3704
|
),
|
|
3668
3705
|
"sand-content": group.get(_InventoryKey.SAND_CONTENT, None),
|
|
3669
3706
|
"carbon-input": group.get(_InventoryKey.CARBON_INPUT, None),
|
|
@@ -3715,8 +3752,17 @@ def _build_inventory_tier_2(
|
|
|
3715
3752
|
}
|
|
3716
3753
|
|
|
3717
3754
|
inventory = merge(grouped_data, grouped_should_run)
|
|
3755
|
+
|
|
3756
|
+
# get a back-up value for sand content if no dated ones are available
|
|
3757
|
+
sand_content = get_node_value(find_term_match(
|
|
3758
|
+
[m for m in measurement_nodes if m.get("depthUpper") == DEPTH_UPPER and m.get("depthLower") == DEPTH_LOWER],
|
|
3759
|
+
SAND_CONTENT_TERM_ID,
|
|
3760
|
+
{}
|
|
3761
|
+
)) / 100
|
|
3762
|
+
|
|
3718
3763
|
kwargs = {
|
|
3719
|
-
"run_with_irrigation": True
|
|
3764
|
+
"run_with_irrigation": True,
|
|
3765
|
+
"sand_content": sand_content
|
|
3720
3766
|
}
|
|
3721
3767
|
|
|
3722
3768
|
return inventory, kwargs
|
|
@@ -3811,7 +3857,7 @@ def _get_grouped_sand_content_measurements(grouped_measurements: dict) -> dict:
|
|
|
3811
3857
|
}
|
|
3812
3858
|
|
|
3813
3859
|
return {
|
|
3814
|
-
year: {_InventoryKey.SAND_CONTENT: get_node_value(measurement)}
|
|
3860
|
+
year: {_InventoryKey.SAND_CONTENT: get_node_value(measurement)/100}
|
|
3815
3861
|
for year, measurement in grouped_sand_content_measurements.items() if measurement
|
|
3816
3862
|
}
|
|
3817
3863
|
|
|
@@ -5,8 +5,6 @@ from hestia_earth.utils.tools import safe_parse_float
|
|
|
5
5
|
from hestia_earth.models.log import debugValues
|
|
6
6
|
from hestia_earth.models.utils.input import get_total_irrigation_m3
|
|
7
7
|
from hestia_earth.models.utils.cycle import get_ecoClimateZone
|
|
8
|
-
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
9
|
-
from hestia_earth.models.utils.blank_node import find_terms_value
|
|
10
8
|
from hestia_earth.models.utils.term import get_lookup_value, get_milkYield_terms
|
|
11
9
|
from hestia_earth.models.utils.ecoClimateZone import get_ecoClimateZone_lookup_value
|
|
12
10
|
from . import MODEL
|
|
@@ -19,16 +17,6 @@ COEFF_N_NH3NOX_organic_animal = [0.21, 0.00, 0.31, 0.0775]
|
|
|
19
17
|
COEFF_N_NH3NOX_inorganic = [0.11, 0.02, 0.33, 0.0775]
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
def get_nh3_no3_nox_to_n(cycle: dict, nh3_term_id: str, no3_term_id: str, nox_term_id: str):
|
|
23
|
-
nh3 = find_terms_value(cycle.get('emissions', []), nh3_term_id)
|
|
24
|
-
nh3 = nh3 / get_atomic_conversion(Units.KG_NH3, Units.TO_N)
|
|
25
|
-
no3 = find_terms_value(cycle.get('emissions', []), no3_term_id)
|
|
26
|
-
no3 = no3 / get_atomic_conversion(Units.KG_NO3, Units.TO_N)
|
|
27
|
-
nox = find_terms_value(cycle.get('emissions', []), nox_term_id)
|
|
28
|
-
nox = nox / get_atomic_conversion(Units.KG_NOX, Units.TO_N)
|
|
29
|
-
return nh3, no3, nox
|
|
30
|
-
|
|
31
|
-
|
|
32
20
|
def get_FracLEACH_H(cycle: dict, term_id: str):
|
|
33
21
|
eco_climate_zone = get_ecoClimateZone(cycle)
|
|
34
22
|
is_eco_climate_zone_dry = eco_climate_zone % 2 == 0
|
|
@@ -130,6 +118,31 @@ N2O_FACTORS = {
|
|
|
130
118
|
'max': 0.029
|
|
131
119
|
}
|
|
132
120
|
}
|
|
121
|
+
EF4_FACTORS = {
|
|
122
|
+
'dry': {
|
|
123
|
+
'value': 0.005,
|
|
124
|
+
'min': 0,
|
|
125
|
+
'max': 0.011
|
|
126
|
+
|
|
127
|
+
},
|
|
128
|
+
'wet': {
|
|
129
|
+
'value': 0.014,
|
|
130
|
+
'min': 0.011,
|
|
131
|
+
'max': 0.017
|
|
132
|
+
},
|
|
133
|
+
'default': {
|
|
134
|
+
'value': 0.01,
|
|
135
|
+
'min': 0.002,
|
|
136
|
+
'max': 0.018
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
EF5_FACTORS = {
|
|
140
|
+
'default': {
|
|
141
|
+
'value': 0.011,
|
|
142
|
+
'min': 0.0,
|
|
143
|
+
'max': 0.02
|
|
144
|
+
}
|
|
145
|
+
}
|
|
133
146
|
|
|
134
147
|
|
|
135
148
|
def _get_waterRegime_lookup(model_term_id: str, practice: dict, col: str):
|
|
@@ -140,11 +153,10 @@ def _is_wet(ecoClimateZone: str = None):
|
|
|
140
153
|
return get_ecoClimateZone_lookup_value(ecoClimateZone, 'wet') == 1 if ecoClimateZone else None
|
|
141
154
|
|
|
142
155
|
|
|
143
|
-
def
|
|
156
|
+
def ecoClimate_factors(factors: dict, input_term_type: TermTermType = None, ecoClimateZone: str = None):
|
|
144
157
|
is_wet = _is_wet(ecoClimateZone)
|
|
145
158
|
factors_key = 'default' if is_wet is None else 'wet' if is_wet else 'dry'
|
|
146
|
-
factors
|
|
147
|
-
return (factors.get(input_term_type) if factors_key == 'wet' else factors, is_wet is None)
|
|
159
|
+
return (factors[factors_key].get(input_term_type, factors[factors_key]), ecoClimateZone is None)
|
|
148
160
|
|
|
149
161
|
|
|
150
162
|
def _flooded_rice_factors(model_term_id: str, cycle: dict):
|
|
@@ -169,4 +181,4 @@ def get_N2O_factors(
|
|
|
169
181
|
flooded_rice: bool = False
|
|
170
182
|
):
|
|
171
183
|
return _flooded_rice_factors(model_term_id, cycle) if flooded_rice \
|
|
172
|
-
else
|
|
184
|
+
else ecoClimate_factors(N2O_FACTORS, input_term_type, ecoClimateZone)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Soil Measurement
|
|
3
|
+
|
|
4
|
+
This model harmonises matching soil measurements into depth ranges of 0-30 and 0-50 and gap fills missing measurements.
|
|
5
|
+
"""
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from hestia_earth.schema import MeasurementMethodClassification
|
|
9
|
+
from hestia_earth.utils.tools import non_empty_list, flatten
|
|
10
|
+
|
|
11
|
+
from hestia_earth.models.log import logRequirements, logShouldRun, logErrorRun
|
|
12
|
+
from hestia_earth.models.utils.measurement import _new_measurement
|
|
13
|
+
from hestia_earth.models.utils.term import get_lookup_value
|
|
14
|
+
from . import MODEL
|
|
15
|
+
|
|
16
|
+
REQUIREMENTS = {
|
|
17
|
+
"Site": {
|
|
18
|
+
"measurements": [
|
|
19
|
+
{"@type": "Measurement", "depthUpper": "", "depthLower": ""}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RETURNS = {
|
|
25
|
+
"Measurement": [{
|
|
26
|
+
"value": "",
|
|
27
|
+
"depthUpper": 0,
|
|
28
|
+
"depthLower": [30, 50],
|
|
29
|
+
"dates": "",
|
|
30
|
+
"methodClassification": "modelled using other measurements"
|
|
31
|
+
}]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
LOOKUPS = {
|
|
35
|
+
"measurement": ["recommendAddingDepth", "depthSensitive"]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
MODEL_KEY = 'soilMeasurement'
|
|
39
|
+
STANDARD_DEPTHS = {(0, 30), (0, 50)}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _measurement(value: float, date: str, term_id: str, standard_fields: dict):
|
|
43
|
+
data = _new_measurement(term=term_id)
|
|
44
|
+
data["value"] = [value]
|
|
45
|
+
data["depthUpper"] = standard_fields["depthUpper"]
|
|
46
|
+
data["depthLower"] = standard_fields["depthLower"]
|
|
47
|
+
data["methodClassification"] = MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS.value
|
|
48
|
+
if date and date[0]:
|
|
49
|
+
data["dates"] = [date]
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_overlap(in_lower: int, in_upper: int, out_lower: int, out_upper: int):
|
|
54
|
+
"""Returns the amount of overlap between upper-lower and range_upper-range_lower."""
|
|
55
|
+
if in_lower >= in_upper or out_lower >= out_upper or in_lower >= out_upper or in_upper <= out_lower:
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
overlap_range = [max(in_lower, out_lower), min(in_upper, out_upper)]
|
|
59
|
+
return max(overlap_range) - min(overlap_range)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _harmonise_measurements(measurements_list: list, standard_depth_lower: int, standard_depth_upper: int) -> float:
|
|
63
|
+
"""Gather measurements and calculate modelled value."""
|
|
64
|
+
total_weight_values = 0
|
|
65
|
+
total_weights = 0
|
|
66
|
+
for measurement_dict in measurements_list:
|
|
67
|
+
value = measurement_dict.get("value", [])[0]
|
|
68
|
+
depth_lower = measurement_dict.get("depthLower", 0)
|
|
69
|
+
depth_upper = measurement_dict.get("depthUpper", 0)
|
|
70
|
+
# Note that the upper/lower here is reversed as lower in the ground (greater depth),
|
|
71
|
+
# means higher numbers.
|
|
72
|
+
weight = _get_overlap(
|
|
73
|
+
in_lower=depth_upper,
|
|
74
|
+
in_upper=depth_lower,
|
|
75
|
+
out_lower=standard_depth_upper,
|
|
76
|
+
out_upper=standard_depth_lower
|
|
77
|
+
)
|
|
78
|
+
total_weights += weight
|
|
79
|
+
total_weight_values += value * weight
|
|
80
|
+
modelled_value = total_weight_values / total_weights if total_weights else 0
|
|
81
|
+
return modelled_value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _expand_multiple_measurements(measurements):
|
|
85
|
+
"""Split/expand measurements with arrays of values and dates into distinct measurements."""
|
|
86
|
+
expanded_measurements = []
|
|
87
|
+
for measurement in measurements:
|
|
88
|
+
if "dates" in measurement and len(measurement.get("value", [])) != len(measurement.get("dates", [])):
|
|
89
|
+
logErrorRun(
|
|
90
|
+
model=MODEL,
|
|
91
|
+
term=measurement.get("term", {}),
|
|
92
|
+
error="Inconsistent field lengths between values and dates fields in measurement."
|
|
93
|
+
)
|
|
94
|
+
elif len(measurement.get("value", [])) < 2:
|
|
95
|
+
expanded_measurements.append(measurement)
|
|
96
|
+
else:
|
|
97
|
+
for v, d in zip(measurement.get("value", []), measurement.get("dates", [])):
|
|
98
|
+
new_measurement = deepcopy(measurement)
|
|
99
|
+
new_measurement.update({"value": [v], "dates": [d]})
|
|
100
|
+
expanded_measurements.append(new_measurement)
|
|
101
|
+
|
|
102
|
+
return expanded_measurements
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _group_measurements_by_date_method_term(measurements):
|
|
106
|
+
group_by_result = defaultdict(list)
|
|
107
|
+
for measurement_dict in measurements:
|
|
108
|
+
dates = measurement_dict.get("dates", [])
|
|
109
|
+
method = measurement_dict.get("method", {}).get("@id", "")
|
|
110
|
+
term_id = measurement_dict.get("term", {}).get("@id", "")
|
|
111
|
+
if not dates:
|
|
112
|
+
dates = [measurement_dict.get('endDate', "")]
|
|
113
|
+
group_by_result[(dates[0], method, term_id)].append(measurement_dict)
|
|
114
|
+
return group_by_result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run_harmonisation(measurements: list, needed_depths: list):
|
|
118
|
+
results = []
|
|
119
|
+
grouped_measurements = _group_measurements_by_date_method_term(
|
|
120
|
+
_expand_multiple_measurements(measurements)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for (date, method, term_id), measurements_list in grouped_measurements.items():
|
|
124
|
+
# For a target depth
|
|
125
|
+
for depth_upper, depth_lower in needed_depths:
|
|
126
|
+
modelled_value = _harmonise_measurements(
|
|
127
|
+
measurements_list=measurements_list,
|
|
128
|
+
standard_depth_upper=depth_upper,
|
|
129
|
+
standard_depth_lower=depth_lower
|
|
130
|
+
)
|
|
131
|
+
if modelled_value:
|
|
132
|
+
results.append(
|
|
133
|
+
_measurement(
|
|
134
|
+
value=modelled_value,
|
|
135
|
+
date=date,
|
|
136
|
+
standard_fields={
|
|
137
|
+
"depthUpper": depth_upper,
|
|
138
|
+
"depthLower": depth_lower
|
|
139
|
+
},
|
|
140
|
+
term_id=term_id
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return results
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _run_gap_fill_depths(measurements_missing_depths: list) -> list:
|
|
148
|
+
return [dict(m, **{"depthUpper": 0, "depthLower": 30}) for m in measurements_missing_depths]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _get_needed_depths(site: dict) -> list:
|
|
152
|
+
needed_depths = list(STANDARD_DEPTHS)
|
|
153
|
+
for measurement in site.get("measurements", []):
|
|
154
|
+
if (measurement.get("depthUpper"), measurement.get("depthLower")) in needed_depths:
|
|
155
|
+
needed_depths.remove((int(measurement["depthUpper"]), int(measurement["depthLower"])))
|
|
156
|
+
|
|
157
|
+
return needed_depths
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _should_run(site: dict, model_key: str):
|
|
161
|
+
# we only work with measurements with depths
|
|
162
|
+
measurements = [
|
|
163
|
+
m for m in site.get("measurements", [])
|
|
164
|
+
if get_lookup_value(m.get("term", {}), LOOKUPS["measurement"][0], model=MODEL, model_key=model_key)
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
measurements_with_depths = [m for m in measurements if all([
|
|
168
|
+
"depthUpper" in m.keys(),
|
|
169
|
+
"depthLower" in m.keys(),
|
|
170
|
+
(int(m.get("depthUpper", 0)), int(m.get("depthLower", 0))) not in STANDARD_DEPTHS
|
|
171
|
+
])]
|
|
172
|
+
has_measurements_with_depths = len(measurements_with_depths) > 0
|
|
173
|
+
|
|
174
|
+
measurements_missing_depth_recommended = [m for m in measurements if all([
|
|
175
|
+
"depthUpper" not in m.keys(),
|
|
176
|
+
"depthLower" not in m.keys(),
|
|
177
|
+
not get_lookup_value(m.get("term", {}), LOOKUPS["measurement"][1], model=MODEL, model_key=model_key)
|
|
178
|
+
])]
|
|
179
|
+
|
|
180
|
+
logRequirements(site, model=MODEL, model_key=model_key,
|
|
181
|
+
has_measurements_with_depths=has_measurements_with_depths,
|
|
182
|
+
has_missing_depths=bool(measurements_missing_depth_recommended))
|
|
183
|
+
|
|
184
|
+
should_run = has_measurements_with_depths or bool(measurements_missing_depth_recommended)
|
|
185
|
+
for measurement in measurements_with_depths + measurements_missing_depth_recommended:
|
|
186
|
+
term_id = measurement.get("term", {}).get("@id", {})
|
|
187
|
+
logShouldRun(site, MODEL, term_id, should_run)
|
|
188
|
+
return should_run, measurements_with_depths, measurements_missing_depth_recommended
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def run(site: dict):
|
|
192
|
+
should_run, measurements_with_depths, measurements_missing_depth = _should_run(site=site, model_key=MODEL_KEY)
|
|
193
|
+
needed_depths = _get_needed_depths(site)
|
|
194
|
+
return non_empty_list(flatten(
|
|
195
|
+
_run_harmonisation(measurements=measurements_with_depths, needed_depths=needed_depths)
|
|
196
|
+
+ _run_gap_fill_depths(measurements_missing_depths=measurements_missing_depth)
|
|
197
|
+
)) if should_run else []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from hestia_earth.schema import CycleFunctionalUnit, SiteSiteType, TermTermType
|
|
2
2
|
from hestia_earth.utils.model import filter_list_term_type, find_term_match, find_primary_product
|
|
3
|
-
from hestia_earth.utils.tools import
|
|
3
|
+
from hestia_earth.utils.tools import list_sum, safe_parse_float, safe_parse_date
|
|
4
4
|
|
|
5
5
|
from ..log import logRequirements, debugValues
|
|
6
6
|
from .lookup import factor_value
|
|
@@ -368,7 +368,7 @@ def is_organic(cycle: dict):
|
|
|
368
368
|
return any([get_lookup_value(p.get('term', {}), 'isOrganic') == 'organic' for p in practices])
|
|
369
369
|
|
|
370
370
|
|
|
371
|
-
def is_irrigated(cycle: dict):
|
|
371
|
+
def is_irrigated(cycle: dict, **log_ars):
|
|
372
372
|
"""
|
|
373
373
|
Check if the `Cycle` is irrigated, i.e. if it contains an irrigated `Practice` with a value above `0`.
|
|
374
374
|
|
|
@@ -376,16 +376,17 @@ def is_irrigated(cycle: dict):
|
|
|
376
376
|
----------
|
|
377
377
|
cycle : dict
|
|
378
378
|
The `Cycle`.
|
|
379
|
+
log_ars : dict[str, Any]
|
|
380
|
+
Extra loggging, e.g. model, term.
|
|
379
381
|
|
|
380
382
|
Returns
|
|
381
383
|
-------
|
|
382
384
|
bool
|
|
383
385
|
`True` if the `Cycle` is irrigated, `False` otherwise.
|
|
384
386
|
"""
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
]
|
|
388
|
-
return list_sum(flatten([p.get('value', []) for p in irrigated_practices])) > 0
|
|
387
|
+
practices = filter_list_term_type(cycle.get('practices', []), TermTermType.WATERREGIME)
|
|
388
|
+
irrigated_practices = [p for p in practices if get_lookup_value(p.get('term', {}), 'irrigated', **log_ars)]
|
|
389
|
+
return any([list_sum(p.get('value', []), 0) > 0 for p in irrigated_practices])
|
|
389
390
|
|
|
390
391
|
|
|
391
392
|
def cycle_end_year(cycle: dict):
|
|
@@ -4,6 +4,8 @@ from hestia_earth.utils.model import linked_node
|
|
|
4
4
|
from hestia_earth.utils.lookup import get_table_value, download_lookup, column_name
|
|
5
5
|
|
|
6
6
|
from . import _term_id, _include_methodModel
|
|
7
|
+
from .blank_node import find_terms_value
|
|
8
|
+
from .constant import Units, get_atomic_conversion
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def _new_emission(term, model=None):
|
|
@@ -17,3 +19,16 @@ def is_in_system_boundary(term_id: str):
|
|
|
17
19
|
value = get_table_value(lookup, 'termid', term_id, column_name('inHestiaDefaultSystemBoundary'))
|
|
18
20
|
# handle numpy boolean
|
|
19
21
|
return not (not value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_nh3_no3_nox_to_n(cycle: dict, nh3_term_id: str, no3_term_id: str, nox_term_id: str, allow_none: bool = False):
|
|
25
|
+
default_value = 0 if allow_none else None
|
|
26
|
+
|
|
27
|
+
nh3 = find_terms_value(cycle.get('emissions', []), nh3_term_id, default=default_value)
|
|
28
|
+
nh3 = None if nh3 is None else nh3 / get_atomic_conversion(Units.KG_NH3, Units.TO_N)
|
|
29
|
+
no3 = find_terms_value(cycle.get('emissions', []), no3_term_id, default=default_value)
|
|
30
|
+
no3 = None if no3 is None else no3 / get_atomic_conversion(Units.KG_NO3, Units.TO_N)
|
|
31
|
+
nox = find_terms_value(cycle.get('emissions', []), nox_term_id, default=default_value)
|
|
32
|
+
nox = None if nox is None else nox / get_atomic_conversion(Units.KG_NOX, Units.TO_N)
|
|
33
|
+
|
|
34
|
+
return (nh3, no3, nox)
|
hestia_earth/models/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = '0.59.
|
|
1
|
+
VERSION = '0.59.3'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hestia-earth-models
|
|
3
|
-
Version: 0.59.
|
|
3
|
+
Version: 0.59.3
|
|
4
4
|
Summary: Hestia's set of modules for filling gaps in the activity data using external datasets (e.g. populating soil properties with a geospatial dataset using provided coordinates) and internal lookups (e.g. populating machinery use from fuel use). Includes rules for when gaps should be filled versus not (e.g. never gap fill yield, gap fill crop residue if yield provided etc.).
|
|
5
5
|
Home-page: https://gitlab.com/hestia-earth/hestia-engine-models
|
|
6
6
|
Author: Hestia Team
|