hestia-earth-models 0.59.1__py3-none-any.whl → 0.59.2__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 (33) hide show
  1. hestia_earth/models/cycle/irrigatedTypeUnspecified.py +6 -7
  2. hestia_earth/models/impact_assessment/irrigated.py +4 -1
  3. hestia_earth/models/ipcc2006/n2OToAirCropResidueDecompositionIndirect.py +3 -3
  4. hestia_earth/models/ipcc2006/n2OToAirExcretaIndirect.py +3 -3
  5. hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserIndirect.py +5 -4
  6. hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserIndirect.py +5 -4
  7. hestia_earth/models/ipcc2006/utils.py +0 -16
  8. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +8 -7
  9. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionIndirect.py +100 -0
  10. hestia_earth/models/ipcc2019/n2OToAirExcretaIndirect.py +100 -0
  11. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +54 -61
  12. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +58 -66
  13. hestia_earth/models/ipcc2019/nh3ToAirOrganicFertiliser.py +104 -0
  14. hestia_earth/models/ipcc2019/noxToAirOrganicFertiliser.py +104 -0
  15. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +56 -18
  16. hestia_earth/models/ipcc2019/utils.py +28 -16
  17. hestia_earth/models/site/soilMeasurement.py +197 -0
  18. hestia_earth/models/utils/cycle.py +7 -6
  19. hestia_earth/models/utils/emission.py +15 -0
  20. hestia_earth/models/version.py +1 -1
  21. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.2.dist-info}/METADATA +1 -1
  22. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.2.dist-info}/RECORD +33 -23
  23. tests/models/ipcc2019/test_n2OToAirCropResidueDecompositionIndirect.py +71 -0
  24. tests/models/ipcc2019/test_n2OToAirExcretaIndirect.py +71 -0
  25. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserIndirect.py +36 -13
  26. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserIndirect.py +36 -13
  27. tests/models/ipcc2019/test_nh3ToAirOrganicFertiliser.py +35 -0
  28. tests/models/ipcc2019/test_noxToAirOrganicFertiliser.py +35 -0
  29. tests/models/ipcc2019/test_organicCarbonPerHa.py +45 -4
  30. tests/models/site/test_soilMeasurement.py +159 -0
  31. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.2.dist-info}/LICENSE +0 -0
  32. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.2.dist-info}/WHEEL +0 -0
  33. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.2.dist-info}/top_level.txt +0 -0
@@ -2,26 +2,49 @@ from unittest.mock import patch
2
2
  import json
3
3
  from tests.utils import fixtures_path, fake_new_emission
4
4
 
5
- from hestia_earth.models.ipcc2019.n2OToAirOrganicFertiliserIndirect import MODEL, TERM_ID, run, _should_run
5
+ from hestia_earth.models.ipcc2019.n2OToAirOrganicFertiliserIndirect import (
6
+ MODEL, TERM_ID, run, _should_run, NH3_TERM_ID, NO3_TERM_ID, NOX_TERM_ID
7
+ )
6
8
 
7
9
  class_path = f"hestia_earth.models.{MODEL}.{TERM_ID}"
8
10
  fixtures_folder = f"{fixtures_path}/{MODEL}/{TERM_ID}"
9
11
 
10
12
 
11
- @patch(f"{class_path}.get_ecoClimateZone", return_value=2)
12
- @patch(f"{class_path}._is_term_type_complete", return_value=False)
13
- @patch(f"{class_path}.get_organic_fertiliser_N_total", return_value=0)
14
- def test_should_run(mock_N_total, mock_complete, *args):
15
- # no N => no run
16
- assert not _should_run({})
13
+ def test_should_run():
14
+ # no emissions => no run
15
+ cycle = {'completeness': {'fertiliser': True}, 'emissions': []}
16
+ should_run, *args = _should_run(cycle)
17
+ assert not should_run
17
18
 
18
- # with N => no run
19
- mock_N_total.return_value = 10
20
- assert not _should_run({})
19
+ # with no3 emission => run
20
+ cycle['emissions'] = [
21
+ {
22
+ 'term': {'@id': NO3_TERM_ID},
23
+ 'value': [100]
24
+ },
25
+ {
26
+ 'term': {'@id': NH3_TERM_ID},
27
+ 'value': [100]
28
+ },
29
+ {
30
+ 'term': {'@id': NOX_TERM_ID},
31
+ 'value': [100]
32
+ }
33
+ ]
34
+ should_run, *args = _should_run(cycle)
35
+ assert should_run is True
21
36
 
22
- # is complete => run
23
- mock_complete.return_value = True
24
- assert _should_run({}) is True
37
+
38
+ @patch(f"{class_path}._new_emission", side_effect=fake_new_emission)
39
+ def test_run(*args):
40
+ with open(f"{fixtures_folder}/cycle.jsonld", encoding='utf-8') as f:
41
+ cycle = json.load(f)
42
+
43
+ with open(f"{fixtures_folder}/result.jsonld", encoding='utf-8') as f:
44
+ expected = json.load(f)
45
+
46
+ value = run(cycle)
47
+ assert value == expected
25
48
 
26
49
 
27
50
  @patch(f"{class_path}._new_emission", side_effect=fake_new_emission)
@@ -0,0 +1,35 @@
1
+ from unittest.mock import patch
2
+ import json
3
+ from tests.utils import fixtures_path, fake_new_emission
4
+
5
+ from hestia_earth.models.ipcc2019.nh3ToAirOrganicFertiliser import MODEL, TERM_ID, run, _should_run
6
+
7
+ class_path = f"hestia_earth.models.{MODEL}.{TERM_ID}"
8
+ fixtures_folder = f"{fixtures_path}/{MODEL}/{TERM_ID}"
9
+
10
+
11
+ @patch(f"{class_path}._is_term_type_complete", return_value=False)
12
+ @patch(f"{class_path}.get_organic_fertiliser_N_total", return_value=0)
13
+ def test_should_run(mock_N_total, mock_complete, *args):
14
+ # no N => no run
15
+ assert not _should_run({})
16
+
17
+ # with N => no run
18
+ mock_N_total.return_value = 10
19
+ assert not _should_run({})
20
+
21
+ # is complete => run
22
+ mock_complete.return_value = True
23
+ assert _should_run({}) is True
24
+
25
+
26
+ @patch(f"{class_path}._new_emission", side_effect=fake_new_emission)
27
+ def test_run(*args):
28
+ with open(f"{fixtures_folder}/cycle.jsonld", encoding='utf-8') as f:
29
+ cycle = json.load(f)
30
+
31
+ with open(f"{fixtures_folder}/result.jsonld", encoding='utf-8') as f:
32
+ expected = json.load(f)
33
+
34
+ value = run(cycle)
35
+ assert value == expected
@@ -0,0 +1,35 @@
1
+ from unittest.mock import patch
2
+ import json
3
+ from tests.utils import fixtures_path, fake_new_emission
4
+
5
+ from hestia_earth.models.ipcc2019.noxToAirOrganicFertiliser import MODEL, TERM_ID, run, _should_run
6
+
7
+ class_path = f"hestia_earth.models.{MODEL}.{TERM_ID}"
8
+ fixtures_folder = f"{fixtures_path}/{MODEL}/{TERM_ID}"
9
+
10
+
11
+ @patch(f"{class_path}._is_term_type_complete", return_value=False)
12
+ @patch(f"{class_path}.get_organic_fertiliser_N_total", return_value=0)
13
+ def test_should_run(mock_N_total, mock_complete, *args):
14
+ # no N => no run
15
+ assert not _should_run({})
16
+
17
+ # with N => no run
18
+ mock_N_total.return_value = 10
19
+ assert not _should_run({})
20
+
21
+ # is complete => run
22
+ mock_complete.return_value = True
23
+ assert _should_run({}) is True
24
+
25
+
26
+ @patch(f"{class_path}._new_emission", side_effect=fake_new_emission)
27
+ def test_run(*args):
28
+ with open(f"{fixtures_folder}/cycle.jsonld", encoding='utf-8') as f:
29
+ cycle = json.load(f)
30
+
31
+ with open(f"{fixtures_folder}/result.jsonld", encoding='utf-8') as f:
32
+ expected = json.load(f)
33
+
34
+ value = run(cycle)
35
+ assert value == expected
@@ -16,6 +16,7 @@ from hestia_earth.models.ipcc2019.organicCarbonPerHa import (
16
16
  _calc_temperature_factor,
17
17
  _calc_tier_1_soc_stocks,
18
18
  _calc_water_factor,
19
+ _calculated_cycles,
19
20
  _check_cropland_low_category,
20
21
  _check_cropland_medium_category,
21
22
  _get_carbon_input_kwargs,
@@ -85,6 +86,25 @@ UPLAND_RICE_CROP_TERM_IDS = [
85
86
  "riceGrainInHuskUpland"
86
87
  ]
87
88
 
89
+ DEFAULT_PROPERTIES = {
90
+ "manureDryKgMass": {
91
+ "carbonContent": {
92
+ "value": 38.4
93
+ },
94
+ "nitrogenContent": {
95
+ "value": 2.65
96
+ },
97
+ "ligninContent": {
98
+ "value": 9.67
99
+ }
100
+ }
101
+ }
102
+
103
+
104
+ def find_term_property_side_effect(term: dict, property: str, *_):
105
+ term_id = term.get('@id', None)
106
+ return DEFAULT_PROPERTIES.get(term_id, {}).get(property, {})
107
+
88
108
 
89
109
  # --- TIER 1 & TIER 2 TESTS ---
90
110
 
@@ -96,11 +116,13 @@ RUN_SUBFOLDERS = [
96
116
  ("tier-2/with-incomplete-climate-data", True), # Closes issue 599
97
117
  ("tier-2/with-initial-soc", True),
98
118
  ("tier-2/with-multi-year-cycles", True),
119
+ ("tier-2/with-multi-year-cycles-and-missing-properties", True), # Closes issue 734
99
120
  ("tier-2/without-any-measurements", True), # Closes issue 594
100
121
  ("tier-2/without-initial-soc", True),
101
122
  ("tier-2/with-irrigation", True),
102
123
  ("tier-2/with-irrigation-dates", True),
103
124
  ("tier-2/with-paddy-rice", True),
125
+ ("tier-2/with-sand-without-date", True), # Closes issue 739
104
126
  ("tier-2/with-irrigated-upland-rice", True),
105
127
  ("tier-1/cropland-depth-as-float", False),
106
128
  ("tier-1/cropland-with-measured-soc", False),
@@ -124,9 +146,11 @@ RUN_SUBFOLDERS = [
124
146
  @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
125
147
  @patch(f"{class_path}.get_upland_rice_land_cover_terms", return_value=UPLAND_RICE_LAND_COVER_TERM_IDS)
126
148
  @patch(f"{class_path}.get_upland_rice_crop_terms", return_value=UPLAND_RICE_CROP_TERM_IDS)
127
- @patch(f"{class_path}.related_cycles")
149
+ @patch(f"{class_path}._calculated_cycles")
150
+ @patch("hestia_earth.models.utils.property.find_term_property")
128
151
  def test_run(
129
- mock_related_cycles,
152
+ mock_find_term_property,
153
+ mock_calculated_cycles,
130
154
  _mock_get_upland_rice_crop_terms,
131
155
  _mock_get_upland_rice_land_cover_terms,
132
156
  _mock_get_residue_removed_or_burnt_terms,
@@ -144,7 +168,8 @@ def test_run(
144
168
  with open(f"{folder}/cycles.jsonld", encoding='utf-8') as f:
145
169
  return json.load(f)
146
170
 
147
- mock_related_cycles.return_value = load_cycles_from_file() if load_cycles else []
171
+ mock_find_term_property.side_effect = find_term_property_side_effect
172
+ mock_calculated_cycles.return_value = load_cycles_from_file() if load_cycles else []
148
173
 
149
174
  with open(f"{folder}/site.jsonld", encoding='utf-8') as f:
150
175
  site = json.load(f)
@@ -164,7 +189,7 @@ def test_run(
164
189
  @patch(f"{class_path}.get_residue_removed_or_burnt_terms", return_value=RESIDUE_REMOVED_OR_BURNT_TERM_IDS)
165
190
  @patch(f"{class_path}.get_upland_rice_land_cover_terms", return_value=UPLAND_RICE_LAND_COVER_TERM_IDS)
166
191
  @patch(f"{class_path}.get_upland_rice_crop_terms", return_value=UPLAND_RICE_CROP_TERM_IDS)
167
- @patch(f"{class_path}.related_cycles", return_value=[])
192
+ @patch(f"{class_path}._calculated_cycles", return_value=[])
168
193
  def test_run_no_data(*args):
169
194
  SITE = {}
170
195
  EXPECTED = []
@@ -209,6 +234,22 @@ def test_calc_water_factor():
209
234
  assert _calc_water_factor(1, 1) == _calc_water_factor(1000, 1000)
210
235
 
211
236
 
237
+ @patch(f"{class_path}.find_related")
238
+ @patch("hestia_earth.models.utils.download_hestia")
239
+ def test_calculated_cycles(mock_download_hestia, mock_find_related):
240
+ CYCLES = [{"@id": "cycle"}]
241
+ EXPECTED = [{"@id": "cycle", "@type": "Cycle"}]
242
+
243
+ mock_download_hestia.side_effect = lambda node_id, node_type, **_: {
244
+ "@type": node_type.value,
245
+ "@id": node_id
246
+ }
247
+ mock_find_related.return_value = CYCLES
248
+
249
+ result = _calculated_cycles({})
250
+ assert EXPECTED == result
251
+
252
+
212
253
  # --- IPCC SOIL CATEGORY TESTS ---
213
254
 
214
255
 
@@ -0,0 +1,159 @@
1
+ from unittest.mock import patch
2
+ import json
3
+ import pytest
4
+
5
+ from tests.utils import fixtures_path, fake_new_measurement
6
+
7
+ from hestia_earth.models.site.soilMeasurement import (
8
+ MODEL,
9
+ MODEL_KEY,
10
+ _get_overlap,
11
+ _harmonise_measurements,
12
+ _should_run, run
13
+ )
14
+
15
+ class_path = f"hestia_earth.models.{MODEL}.{MODEL_KEY}"
16
+ fixtures_folder = f"{fixtures_path}/site/soilMeasurement"
17
+
18
+
19
+ @pytest.mark.parametrize(
20
+ "data,expected",
21
+ [
22
+ ((1, 10, 2, 150), 8),
23
+ ((1, 10, 5, 15), 5),
24
+ ((1, 10, 10, 15), 0),
25
+ ((10, 10, 0, 150), 0),
26
+ ((150, 155, 1, 150), 0),
27
+ ((20, 40, 0, 30), 10),
28
+ ((0, 20, 0, 50), 20),
29
+ ((20, 60, 0, 50), 30),
30
+ ((10, 20, 40, 50), 0),
31
+ ((10, 20, 19, 50), 1),
32
+ ]
33
+ )
34
+ def test_get_overlap(data, expected):
35
+ result = _get_overlap(
36
+ data[0], data[1], data[2], data[3]
37
+ )
38
+ assert result == expected
39
+
40
+
41
+ @pytest.mark.parametrize(
42
+ "measurements_list,returns_dict,expected_value",
43
+ [
44
+ (
45
+ [
46
+ {"value": [7.5], "depthUpper": 0, "depthLower": 20},
47
+ {"value": [6], "depthUpper": 20, "depthLower": 40},
48
+ ],
49
+ {"depthUpper": 0, "depthLower": 30},
50
+ 7
51
+ ),
52
+ (
53
+ [
54
+ {"value": [7.5], "depthUpper": 0, "depthLower": 20},
55
+ {"value": [6], "depthUpper": 20, "depthLower": 40},
56
+ ],
57
+ {"depthUpper": 0, "depthLower": 50},
58
+ 6.75
59
+ ),
60
+ ]
61
+ )
62
+ def test_harmonise_measurements(measurements_list, returns_dict, expected_value):
63
+ actual_value = _harmonise_measurements(
64
+ measurements_list=measurements_list,
65
+ standard_depth_upper=returns_dict["depthUpper"],
66
+ standard_depth_lower=returns_dict["depthLower"],
67
+ )
68
+ assert actual_value == expected_value
69
+
70
+
71
+ @pytest.mark.parametrize(
72
+ "test_name,site,expected_should_run",
73
+ [
74
+ (
75
+ "no measurement => no run",
76
+ {"measurements": []},
77
+ False
78
+ ),
79
+ (
80
+ "missing dates => run",
81
+ {
82
+ "measurements":
83
+ [
84
+ {
85
+ "term": {"@id": "clayContent"},
86
+ "depthUpper": 0,
87
+ "depthLower": 20
88
+ }
89
+ ]
90
+ },
91
+ True
92
+ ),
93
+ (
94
+ "no depthUpper => no run",
95
+ {
96
+ "measurements":
97
+ [
98
+ {
99
+ "term": {"@id": "clayContent"},
100
+ "dates": ["2022-01-02"],
101
+ "depthLower": 20
102
+ }
103
+ ]
104
+ },
105
+ False
106
+ ),
107
+ (
108
+ "all fields => run",
109
+ {
110
+ "measurements":
111
+ [
112
+ {
113
+ "term": {"@id": "clayContent"},
114
+ "dates": ["2022-01-02"],
115
+ "depthUpper": 0,
116
+ "depthLower": 20
117
+ }
118
+ ]
119
+ },
120
+ True
121
+ )
122
+ ]
123
+ )
124
+ @patch(f"{class_path}.get_lookup_value")
125
+ def test_should_run(mock_get_lookup, test_name, site, expected_should_run):
126
+ mock_get_lookup.return_value = True
127
+ model_key = "clayContent"
128
+ should_run, *args = _should_run(site=site, model_key=model_key)
129
+ assert should_run == expected_should_run, test_name
130
+
131
+
132
+ def lookup_side_effect(*args, **kwargs):
133
+ _ = kwargs
134
+ if args[0]["@id"] == "soilPh" and args[1] == "depthSensitive":
135
+ return False
136
+ elif args[0]["@id"] in {"baseSaturation", "soilDepth", "rainfallHourly"}:
137
+ return False
138
+ return True
139
+
140
+
141
+ @pytest.mark.parametrize(
142
+ "test_name",
143
+ [
144
+ "missingDepth", "simpleSoilPh", "clayContent", "missingDepth", "nonUniqueMeasurements", "arrays"
145
+ ]
146
+ )
147
+ @patch(f"{class_path}._new_measurement", side_effect=fake_new_measurement)
148
+ @patch(f"{class_path}.get_lookup_value")
149
+ def test_run(mock_lookup, mock_new_measurement, test_name):
150
+ mock_lookup.side_effect = lookup_side_effect
151
+
152
+ with open(f"{fixtures_folder}/{test_name}/site.jsonld", encoding='utf-8') as f:
153
+ site = json.load(f)
154
+
155
+ with open(f"{fixtures_folder}/{test_name}/result.jsonld", encoding='utf-8') as f:
156
+ result = json.load(f)
157
+
158
+ value = run(site)
159
+ assert value == result