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.

Files changed (40) 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/n2OToAirCropResidueDecompositionDirect.py +83 -0
  4. hestia_earth/models/ipcc2006/n2OToAirCropResidueDecompositionIndirect.py +3 -3
  5. hestia_earth/models/ipcc2006/n2OToAirExcretaIndirect.py +3 -3
  6. hestia_earth/models/ipcc2006/n2OToAirInorganicFertiliserIndirect.py +5 -4
  7. hestia_earth/models/ipcc2006/n2OToAirOrganicFertiliserIndirect.py +5 -4
  8. hestia_earth/models/ipcc2006/utils.py +12 -16
  9. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionDirect.py +8 -7
  10. hestia_earth/models/ipcc2019/n2OToAirCropResidueDecompositionIndirect.py +100 -0
  11. hestia_earth/models/ipcc2019/n2OToAirExcretaIndirect.py +100 -0
  12. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +54 -61
  13. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +58 -66
  14. hestia_earth/models/ipcc2019/nh3ToAirInorganicFertiliser.py +112 -0
  15. hestia_earth/models/ipcc2019/nh3ToAirOrganicFertiliser.py +107 -0
  16. hestia_earth/models/ipcc2019/noxToAirInorganicFertiliser.py +112 -0
  17. hestia_earth/models/ipcc2019/noxToAirOrganicFertiliser.py +107 -0
  18. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +67 -21
  19. hestia_earth/models/ipcc2019/utils.py +28 -16
  20. hestia_earth/models/site/soilMeasurement.py +197 -0
  21. hestia_earth/models/utils/cycle.py +7 -6
  22. hestia_earth/models/utils/emission.py +15 -0
  23. hestia_earth/models/version.py +1 -1
  24. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/METADATA +1 -1
  25. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/RECORD +40 -24
  26. tests/models/ipcc2006/test_n2OToAirCropResidueDecompositionDirect.py +50 -0
  27. tests/models/ipcc2019/test_n2OToAirCropResidueDecompositionIndirect.py +71 -0
  28. tests/models/ipcc2019/test_n2OToAirExcretaIndirect.py +71 -0
  29. tests/models/ipcc2019/test_n2OToAirInorganicFertiliserIndirect.py +36 -13
  30. tests/models/ipcc2019/test_n2OToAirOrganicFertiliserIndirect.py +36 -13
  31. tests/models/ipcc2019/test_nh3ToAirInorganicFertiliser.py +47 -0
  32. tests/models/ipcc2019/test_nh3ToAirOrganicFertiliser.py +35 -0
  33. tests/models/ipcc2019/test_noxToAirInorganicFertiliser.py +47 -0
  34. tests/models/ipcc2019/test_noxToAirOrganicFertiliser.py +35 -0
  35. tests/models/ipcc2019/test_organicCarbonPerHa.py +51 -5
  36. tests/models/site/test_soilMeasurement.py +159 -0
  37. tests/models/utils/test_blank_node.py +5 -5
  38. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/LICENSE +0 -0
  39. {hestia_earth_models-0.59.1.dist-info → hestia_earth_models-0.59.3.dist-info}/WHEEL +0 -0
  40. {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": "organicFertiliserOrSoilCarbonIncreasingAmendmentUsed"
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
- CROP_RESIDUE_PROPERTY_TERM_IDS = [
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 = "organicFertiliserOrSoilCarbonIncreasingAmendmentUsed"
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, False).get("value", 0)/100 for term_id in CROP_RESIDUE_PROPERTY_TERM_IDS
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([_iterate_carbon_source(node) for node in inputs_and_products])
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
- group[_InventoryKey.SAND_CONTENT]/100 for group in valid_inventory.values()
1956
- if _InventoryKey.SAND_CONTENT in group
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 = related_cycles(site.get("@id"))
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.PET_MONTHLY) else None
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 _ecoClimate_factors(input_term_type: TermTermType, ecoClimateZone: str = None):
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 = N2O_FACTORS[factors_key]
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 _ecoClimate_factors(input_term_type, ecoClimateZone)
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 flatten, list_sum, safe_parse_float, safe_parse_date
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
- irrigated_practices = [
386
- p for p in cycle.get('practices', []) if p.get('term', {}).get('@id', '').startswith('irrigated')
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)
@@ -1 +1 @@
1
- VERSION = '0.59.1'
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.1
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