hestia-earth-models 0.65.11__py3-none-any.whl → 0.67.0__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.
Files changed (86) hide show
  1. hestia_earth/models/cache_sites.py +7 -9
  2. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +23 -54
  3. hestia_earth/models/cml2001Baseline/resourceUseEnergyDepletionDuringCycle.py +152 -0
  4. hestia_earth/models/cml2001Baseline/resourceUseEnergyDepletionInputsProduction.py +40 -0
  5. hestia_earth/models/cml2001Baseline/resourceUseMineralsAndMetalsDuringCycle.py +80 -0
  6. hestia_earth/models/cml2001Baseline/resourceUseMineralsAndMetalsInputsProduction.py +40 -0
  7. hestia_earth/models/config/Cycle.json +34 -16
  8. hestia_earth/models/config/ImpactAssessment.json +1867 -1832
  9. hestia_earth/models/config/Site.json +4 -1
  10. hestia_earth/models/cycle/completeness/freshForage.py +10 -2
  11. hestia_earth/models/cycle/cropResidueManagement.py +3 -1
  12. hestia_earth/models/cycle/input/hestiaAggregatedData.py +13 -10
  13. hestia_earth/models/ecoinventV3/__init__.py +2 -1
  14. hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/__init__.py +4 -3
  15. hestia_earth/models/environmentalFootprintV3_1/environmentalFootprintSingleOverallScore.py +135 -0
  16. hestia_earth/models/environmentalFootprintV3_1/marineEutrophicationPotential.py +36 -0
  17. hestia_earth/models/environmentalFootprintV3_1/scarcityWeightedWaterUse.py +40 -0
  18. hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/soilQualityIndexLandTransformation.py +17 -6
  19. hestia_earth/models/geospatialDatabase/{aware.py → awareWaterBasinId.py} +1 -1
  20. hestia_earth/models/hestia/landCover.py +42 -34
  21. hestia_earth/models/hestia/residueRemoved.py +80 -0
  22. hestia_earth/models/hestia/resourceUse_utils.py +43 -29
  23. hestia_earth/models/impact_assessment/product/value.py +1 -1
  24. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +34 -13
  25. hestia_earth/models/ipcc2019/belowGroundBiomass.py +33 -12
  26. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +17 -8
  27. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +7 -4
  28. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2 -1
  29. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +29 -18
  30. hestia_earth/models/ipcc2019/pastureGrass_utils.py +8 -1
  31. hestia_earth/models/log.py +1 -1
  32. hestia_earth/models/mocking/search-results.json +872 -872
  33. hestia_earth/models/site/defaultMethodClassification.py +9 -2
  34. hestia_earth/models/site/defaultMethodClassificationDescription.py +4 -2
  35. hestia_earth/models/site/management.py +48 -30
  36. hestia_earth/models/site/pre_checks/cache_geospatialDatabase.py +19 -14
  37. hestia_earth/models/utils/__init__.py +6 -0
  38. hestia_earth/models/utils/aggregated.py +13 -10
  39. hestia_earth/models/utils/array_builders.py +4 -3
  40. hestia_earth/models/utils/blank_node.py +23 -13
  41. hestia_earth/models/utils/lookup.py +4 -2
  42. hestia_earth/models/utils/property.py +5 -2
  43. hestia_earth/models/version.py +1 -1
  44. hestia_earth/orchestrator/log.py +11 -0
  45. hestia_earth/orchestrator/models/__init__.py +8 -3
  46. hestia_earth/orchestrator/strategies/merge/merge_list.py +17 -6
  47. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/METADATA +1 -1
  48. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/RECORD +86 -69
  49. tests/models/cml2001Baseline/test_abioticResourceDepletionFossilFuels.py +51 -87
  50. tests/models/cml2001Baseline/test_resourceUseEnergyDepletionDuringCycle.py +103 -0
  51. tests/models/cml2001Baseline/test_resourceUseEnergyDepletionInputsProduction.py +23 -0
  52. tests/models/cml2001Baseline/test_resourceUseMineralsAndMetalsDuringCycle.py +58 -0
  53. tests/models/cml2001Baseline/test_resourceUseMineralsAndMetalsInputsProduction.py +23 -0
  54. tests/models/environmentalFootprintV3_1/test_environmentalFootprintSingleOverallScore.py +93 -0
  55. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_freshwaterEcotoxicityPotentialCtue.py +6 -5
  56. tests/models/environmentalFootprintV3_1/test_marineEutrophicationPotential.py +27 -0
  57. tests/models/environmentalFootprintV3_1/test_scarcityWeightedWaterUse.py +32 -0
  58. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_soilQualityIndexLandOccupation.py +4 -3
  59. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_soilQualityIndexLandTransformation.py +8 -22
  60. tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/test_soilQualityIndexTotalLandUseEffects.py +4 -4
  61. tests/models/faostat2018/product/test_price.py +1 -1
  62. tests/models/geospatialDatabase/{test_aware.py → test_awareWaterBasinId.py} +1 -1
  63. tests/models/hestia/test_landCover.py +2 -1
  64. tests/models/hestia/test_landTransformation20YearAverageDuringCycle.py +2 -1
  65. tests/models/hestia/test_residueRemoved.py +20 -0
  66. tests/models/impact_assessment/test_emissions.py +0 -1
  67. tests/models/ipcc2019/test_aboveGroundBiomass.py +3 -1
  68. tests/models/ipcc2019/test_belowGroundBiomass.py +4 -2
  69. tests/models/ipcc2019/test_organicCarbonPerHa.py +94 -1
  70. tests/models/site/pre_checks/test_cache_geospatialDatabase.py +22 -0
  71. tests/models/site/test_defaultMethodClassification.py +6 -0
  72. tests/models/site/test_defaultMethodClassificationDescription.py +6 -0
  73. tests/models/site/test_management.py +4 -4
  74. tests/models/test_cache_sites.py +2 -2
  75. tests/models/test_config.py +3 -3
  76. tests/models/test_ecoinventV3.py +0 -1
  77. tests/models/utils/test_array_builders.py +2 -2
  78. tests/orchestrator/strategies/merge/test_merge_list.py +11 -1
  79. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/freshwaterEcotoxicityPotentialCtue.py +0 -0
  80. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/soilQualityIndexLandOccupation.py +0 -0
  81. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/soilQualityIndexTotalLandUseEffects.py +0 -0
  82. /hestia_earth/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/utils.py +0 -0
  83. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/LICENSE +0 -0
  84. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/WHEEL +0 -0
  85. {hestia_earth_models-0.65.11.dist-info → hestia_earth_models-0.67.0.dist-info}/top_level.txt +0 -0
  86. /tests/models/{environmentalFootprintV3 → environmentalFootprintV3_1}/__init__.py +0 -0
@@ -0,0 +1,80 @@
1
+ """
2
+ This model will gap-fill the value for `residueRemoved` when the only provided data is the incorporated residue.
3
+ We are assuming that anything that was not incorporated must have been removed.
4
+ """
5
+ from hestia_earth.schema import TermTermType
6
+ from hestia_earth.utils.model import filter_list_term_type
7
+ from hestia_earth.utils.tools import list_sum
8
+
9
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
10
+ from hestia_earth.models.utils.completeness import _is_term_type_incomplete
11
+ from hestia_earth.models.utils.practice import _new_practice
12
+ from hestia_earth.models.utils import is_from_model
13
+ from . import MODEL
14
+
15
+ REQUIREMENTS = {
16
+ "Cycle": {
17
+ "completeness.cropResidue": "False",
18
+ "practices": [{
19
+ "@type": "Practice",
20
+ "term.@id": [
21
+ "residueIncorporated",
22
+ "residueIncorporatedLessThan30DaysBeforeCultivation",
23
+ "residueIncorporatedMoreThan30DaysBeforeCultivation"
24
+ ]
25
+ }],
26
+ "none": {
27
+ "practices": [{
28
+ "@type": "Practice",
29
+ "term.@id": [
30
+ "residueRemoved",
31
+ "residueBurnt",
32
+ "residueLeftOnField"
33
+ ]
34
+ }]
35
+ }
36
+ }
37
+ }
38
+ RETURNS = {
39
+ "Practice": [{
40
+ "value": ""
41
+ }]
42
+ }
43
+ TERM_ID = 'residueRemoved'
44
+
45
+
46
+ def _practice(value: float):
47
+ practice = _new_practice(TERM_ID, MODEL)
48
+ practice['value'] = [value]
49
+ return practice
50
+
51
+
52
+ def _should_run(cycle: dict):
53
+ crop_residue_incomplete = _is_term_type_incomplete(cycle, TermTermType.CROPRESIDUE)
54
+
55
+ practices = filter_list_term_type(cycle.get('practices', []), TermTermType.CROPRESIDUEMANAGEMENT)
56
+ incorporated_practices = [
57
+ {'id': p.get('term', {}).get('@id'), 'value': list_sum(p.get('value'), None)}
58
+ for p in practices
59
+ if p.get('term', {}).get('@id').startswith('residueIncorporated') and not is_from_model(p)
60
+ ]
61
+ has_other_practices = any([
62
+ not p.get('term', {}).get('@id').startswith('residueIncorporated')
63
+ for p in practices
64
+ ])
65
+ incorporated_value = list_sum([p.get('value') for p in incorporated_practices], None)
66
+
67
+ logRequirements(cycle, model=MODEL, term=TERM_ID,
68
+ term_type_cropResidue_incomplete=crop_residue_incomplete,
69
+ incorporated_practices=log_as_table(incorporated_practices),
70
+ incorporated_value=incorporated_value,
71
+ has_other_practices=has_other_practices)
72
+
73
+ should_run = all([crop_residue_incomplete, incorporated_value, not has_other_practices])
74
+ logShouldRun(cycle, MODEL, TERM_ID, should_run)
75
+ return should_run, 100 - (incorporated_value or 0)
76
+
77
+
78
+ def run(cycle: dict):
79
+ should_run, value = _should_run(cycle)
80
+ return [_practice(value)] if should_run else []
@@ -46,26 +46,25 @@ def _gap_filled_date_obj(date_str: str) -> datetime:
46
46
  )
47
47
 
48
48
 
49
- def _should_run_close_date_found(
50
- ia_end_date_str: str,
49
+ def _find_closest_node(
50
+ ia_date_str: str,
51
51
  management_nodes: list,
52
- historic_date_offset: int
53
- ) -> tuple[bool, str]:
52
+ historic_date_offset: int,
53
+ node_date_field: str
54
+ ) -> str:
54
55
  historic_ia_date_obj = (
55
- _gap_filled_date_obj(ia_end_date_str) - relativedelta(years=historic_date_offset)
56
- if ia_end_date_str else None
56
+ _gap_filled_date_obj(ia_date_str) - relativedelta(years=historic_date_offset)
57
+ if ia_date_str else None
57
58
  )
58
59
  # Calculate all distances in days which are less than MAXIMUM_OFFSET_DAYS from historic date
59
- # Assumption: if there are two dates are equidistant from the target chose the second.
60
+ # Assumption: if there are two dates are equidistant from the target, choose the second.
60
61
  filtered_dates = {
61
- abs((_gap_filled_date_obj(node.get("endDate")) - historic_ia_date_obj).days): node.get("endDate")
62
+ abs((_gap_filled_date_obj(node.get(node_date_field)) - historic_ia_date_obj).days): node.get(node_date_field)
62
63
  for node in management_nodes
63
64
  if node.get("term", {}).get("termType", "") == TermTermType.LANDCOVER.value and
64
- abs((_gap_filled_date_obj(node.get("endDate")) - historic_ia_date_obj).days) <= _MAXIMUM_OFFSET_DAYS
65
+ abs((_gap_filled_date_obj(node.get(node_date_field)) - historic_ia_date_obj).days) <= _MAXIMUM_OFFSET_DAYS
65
66
  }
66
- nearest_date = filtered_dates[min(filtered_dates.keys())] if filtered_dates else ""
67
-
68
- return nearest_date != "", nearest_date
67
+ return filtered_dates[min(filtered_dates.keys())] if filtered_dates else ""
69
68
 
70
69
 
71
70
  def should_run(
@@ -73,7 +72,7 @@ def should_run(
73
72
  site: dict,
74
73
  term_id: str,
75
74
  historic_date_offset: int
76
- ) -> tuple[bool, dict, str]:
75
+ ) -> tuple[bool, dict, str, str]:
77
76
  relevant_emission_resource_use = [
78
77
  node for node in impact_assessment.get("emissionsResourceUse", [])
79
78
  if node.get("term", {}).get("@id", "") == _RESOURCE_USE_TERM_ID and node.get("value", -1) >= 0
@@ -87,33 +86,45 @@ def should_run(
87
86
  {node.get("landCover", {}).get("@id") for node in relevant_emission_resource_use}
88
87
  for node in filtered_management_nodes
89
88
  )
89
+ match_mode = (
90
+ DatestrGapfillMode.START if impact_assessment.get("cycle", {}).get("aggregated") is True
91
+ else DatestrGapfillMode.END
92
+ )
93
+ match_date = "startDate" if match_mode == DatestrGapfillMode.START else "endDate"
94
+
95
+ closest_date = _find_closest_node(
96
+ ia_date_str=impact_assessment.get(match_date, ""),
97
+ management_nodes=filtered_management_nodes,
98
+ historic_date_offset=historic_date_offset,
99
+ node_date_field=match_date
100
+ )
101
+ closest_start_date, closest_end_date = (closest_date, None) if match_date == "startDate" else (None, closest_date)
90
102
  current_node_index = next(
91
103
  (i for i, node in enumerate(filtered_management_nodes)
92
- if _str_dates_match(node.get("endDate", ""), impact_assessment.get("endDate", ""))),
104
+ if _str_dates_match(
105
+ date_str_one=node.get(match_date, ""),
106
+ date_str_two=impact_assessment.get(match_date, ""),
107
+ mode=match_mode
108
+ )),
93
109
  None
94
110
  )
95
111
  current_node = filtered_management_nodes.pop(current_node_index) if current_node_index is not None else None
96
112
 
97
- close_date_found, closest_date_str = _should_run_close_date_found(
98
- ia_end_date_str=impact_assessment.get("endDate", ""),
99
- management_nodes=filtered_management_nodes,
100
- historic_date_offset=historic_date_offset
101
- )
102
-
103
113
  logRequirements(impact_assessment, model=MODEL, term=term_id,
104
- closest_date=closest_date_str,
105
- land_occupation_during_cycle_found=land_occupation_during_cycle_found,
106
- land_cover_term_id=(current_node or {}).get('term', {}).get('@id'))
114
+ closest_end_date=closest_end_date,
115
+ closest_start_date=closest_start_date,
116
+ has_landOccupationDuringCycle=land_occupation_during_cycle_found,
117
+ landCover_term_id=(current_node or {}).get('term', {}).get('@id'))
107
118
 
108
119
  should_run_result = all([
109
120
  relevant_emission_resource_use,
110
121
  land_occupation_during_cycle_found,
111
122
  current_node,
112
- close_date_found
123
+ closest_end_date or closest_start_date
113
124
  ])
114
125
  logShouldRun(impact_assessment, MODEL, term=term_id, should_run=should_run_result)
115
126
 
116
- return should_run_result, current_node, closest_date_str
127
+ return should_run_result, current_node, closest_end_date, closest_start_date
117
128
 
118
129
 
119
130
  def _get_land_occupation_for_land_use_type(impact_assessment: dict, ipcc_land_use_category: str) -> float:
@@ -160,7 +171,8 @@ def _calculate_indicator_value(
160
171
  def _run_calculate_transformation(
161
172
  term_id: str,
162
173
  current_node: dict,
163
- closest_date_str: str,
174
+ closest_end_date: str,
175
+ closest_start_date: str,
164
176
  impact_assessment: dict,
165
177
  site: dict,
166
178
  historic_date_offset: int
@@ -178,7 +190,8 @@ def _run_calculate_transformation(
178
190
  term_id=term_id,
179
191
  management_nodes=[
180
192
  node for node in site.get("management", [])
181
- if _str_dates_match(node.get("endDate", ""), closest_date_str)
193
+ if _str_dates_match(node.get("endDate", ""), closest_end_date) or
194
+ _str_dates_match(node.get("startDate", ""), closest_start_date)
182
195
  ],
183
196
  ipcc_land_use_category=crop_ipcc_land_use_category(current_node.get("term", {}).get("@id", "")),
184
197
  previous_land_cover_id=previous_land_cover_id,
@@ -196,7 +209,7 @@ def run_resource_use(
196
209
  term_id: str
197
210
  ) -> list:
198
211
  site = get_site(impact_assessment)
199
- _should_run, current_node, closest_date_str = should_run(
212
+ _should_run, current_node, closest_end_date, closest_start_date = should_run(
200
213
  impact_assessment=impact_assessment,
201
214
  site=site,
202
215
  term_id=term_id,
@@ -207,6 +220,7 @@ def run_resource_use(
207
220
  site=site,
208
221
  term_id=term_id,
209
222
  current_node=current_node,
210
- closest_date_str=closest_date_str,
223
+ closest_end_date=closest_end_date,
224
+ closest_start_date=closest_start_date,
211
225
  historic_date_offset=historic_date_offset
212
226
  ) if _should_run else []
@@ -25,7 +25,7 @@ MODEL_KEY = 'value'
25
25
 
26
26
 
27
27
  def _run(impact: dict, product: dict):
28
- return {**impact.get('product'), MODEL_KEY: product.get(MODEL_KEY, [])}
28
+ return impact.get('product') | {MODEL_KEY: product.get(MODEL_KEY, [])}
29
29
 
30
30
 
31
31
  def _should_run(impact: dict):
@@ -170,7 +170,7 @@ def _should_run(site: dict) -> tuple[bool, dict, dict]:
170
170
  inventory = _compile_inventory(land_cover) if should_compile_inventory else {}
171
171
  kwargs = {
172
172
  "eco_climate_zone": eco_climate_zone,
173
- "seed": gen_seed(site)
173
+ "seed": gen_seed(site, MODEL, TERM_ID)
174
174
  }
175
175
 
176
176
  logRequirements(
@@ -229,6 +229,7 @@ def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
229
229
  The inventory of data.
230
230
  """
231
231
  land_cover_grouped = group_nodes_by_year(land_cover_nodes)
232
+ min_year, max_year = min(land_cover_grouped.keys()), max(land_cover_grouped.keys())
232
233
 
233
234
  def build_inventory_year(inventory: dict, year_pair: tuple[int, int]) -> dict:
234
235
  """
@@ -262,15 +263,33 @@ def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
262
263
  years_since_lcc_event = time_delta if is_lcc_event else prev_years_since_lcc_event + time_delta
263
264
  regime_start_year = current_year - years_since_lcc_event
264
265
 
266
+ equilibrium_year = regime_start_year + _EQUILIBRIUM_TRANSITION_PERIOD
267
+ inventory_years = set(list(inventory.keys()) + list(land_cover_grouped.keys()))
268
+
269
+ should_add_equilibrium_year = (
270
+ min_year < equilibrium_year < max_year # Is the year relevant?
271
+ and equilibrium_year not in inventory_years # Is the year missing?
272
+ and equilibrium_year < current_year # Is it the first inventory year after the equilibrium?
273
+ )
274
+
275
+ current_data = {
276
+ _InventoryKey.BIOMASS_CATEGORY_SUMMARY: biomass_category_summary,
277
+ _InventoryKey.LAND_COVER_SUMMARY: land_cover_summary,
278
+ _InventoryKey.LAND_COVER_CHANGE_EVENT: is_lcc_event,
279
+ _InventoryKey.YEARS_SINCE_LCC_EVENT: years_since_lcc_event,
280
+ _InventoryKey.REGIME_START_YEAR: regime_start_year
281
+ }
282
+
283
+ equilibrium_data = {
284
+ **current_data,
285
+ _InventoryKey.YEARS_SINCE_LCC_EVENT: _EQUILIBRIUM_TRANSITION_PERIOD
286
+ }
287
+
265
288
  update_dict = {
266
- current_year: {
267
- _InventoryKey.BIOMASS_CATEGORY_SUMMARY: biomass_category_summary,
268
- _InventoryKey.LAND_COVER_SUMMARY: land_cover_summary,
269
- _InventoryKey.LAND_COVER_CHANGE_EVENT: is_lcc_event,
270
- _InventoryKey.YEARS_SINCE_LCC_EVENT: years_since_lcc_event,
271
- _InventoryKey.REGIME_START_YEAR: regime_start_year
272
- }
289
+ current_year: current_data,
290
+ **({equilibrium_year: equilibrium_data} if should_add_equilibrium_year else {})
273
291
  }
292
+
274
293
  return inventory | update_dict
275
294
 
276
295
  start_year = list(land_cover_grouped)[0]
@@ -290,11 +309,13 @@ def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
290
309
  }
291
310
  }
292
311
 
293
- return reduce(
294
- build_inventory_year,
295
- pairwise(land_cover_grouped.keys()), # Inventory years need data from previous year to be compiled.
296
- initial
297
- )
312
+ return dict(sorted(
313
+ reduce(
314
+ build_inventory_year,
315
+ pairwise(land_cover_grouped.keys()), # Inventory years need data from previous year to be compiled.
316
+ initial
317
+ ).items()
318
+ ))
298
319
 
299
320
 
300
321
  def _format_inventory(inventory: dict) -> str:
@@ -167,7 +167,7 @@ def _should_run(site: dict) -> tuple[bool, dict, dict]:
167
167
  inventory = _compile_inventory(land_cover) if should_compile_inventory else {}
168
168
  kwargs = {
169
169
  "eco_climate_zone": eco_climate_zone,
170
- "seed": gen_seed(site)
170
+ "seed": gen_seed(site, MODEL, TERM_ID)
171
171
  }
172
172
 
173
173
  logRequirements(
@@ -222,6 +222,7 @@ def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
222
222
  The inventory of data.
223
223
  """
224
224
  land_cover_grouped = group_nodes_by_year(land_cover_nodes)
225
+ min_year, max_year = min(land_cover_grouped.keys()), max(land_cover_grouped.keys())
225
226
 
226
227
  def build_inventory_year(inventory: dict, year_pair: tuple[int, int]) -> dict:
227
228
  """
@@ -253,14 +254,32 @@ def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
253
254
  years_since_lcc_event = time_delta if is_lcc_event else prev_years_since_lcc_event + time_delta
254
255
  regime_start_year = current_year - years_since_lcc_event
255
256
 
257
+ equilibrium_year = regime_start_year + _EQUILIBRIUM_TRANSITION_PERIOD
258
+ inventory_years = set(list(inventory.keys()) + list(land_cover_grouped.keys()))
259
+
260
+ should_add_equilibrium_year = (
261
+ min_year < equilibrium_year < max_year # Is the year relevant?
262
+ and equilibrium_year not in inventory_years # Is the year missing?
263
+ and equilibrium_year < current_year # Is it the first inventory year after the equilibrium?
264
+ )
265
+
266
+ current_data = {
267
+ _InventoryKey.BIOMASS_CATEGORY_SUMMARY: biomass_category_summary,
268
+ _InventoryKey.LAND_COVER_CHANGE_EVENT: is_lcc_event,
269
+ _InventoryKey.YEARS_SINCE_LCC_EVENT: years_since_lcc_event,
270
+ _InventoryKey.REGIME_START_YEAR: regime_start_year
271
+ }
272
+
273
+ equilibrium_data = {
274
+ **current_data,
275
+ _InventoryKey.YEARS_SINCE_LCC_EVENT: _EQUILIBRIUM_TRANSITION_PERIOD
276
+ }
277
+
256
278
  update_dict = {
257
- current_year: {
258
- _InventoryKey.BIOMASS_CATEGORY_SUMMARY: biomass_category_summary,
259
- _InventoryKey.LAND_COVER_CHANGE_EVENT: is_lcc_event,
260
- _InventoryKey.YEARS_SINCE_LCC_EVENT: years_since_lcc_event,
261
- _InventoryKey.REGIME_START_YEAR: regime_start_year
262
- }
279
+ current_year: current_data,
280
+ **({equilibrium_year: equilibrium_data} if should_add_equilibrium_year else {})
263
281
  }
282
+
264
283
  return inventory | update_dict
265
284
 
266
285
  start_year = list(land_cover_grouped)[0]
@@ -277,11 +296,13 @@ def _compile_inventory(land_cover_nodes: list[dict]) -> dict:
277
296
  }
278
297
  }
279
298
 
280
- return reduce(
281
- build_inventory_year,
282
- pairwise(land_cover_grouped.keys()), # Inventory years need data from previous year to be compiled.
283
- initial
284
- )
299
+ return dict(sorted(
300
+ reduce(
301
+ build_inventory_year,
302
+ pairwise(land_cover_grouped.keys()), # Inventory years need data from previous year to be compiled.
303
+ initial
304
+ ).items()
305
+ ))
285
306
 
286
307
 
287
308
  def _format_inventory(inventory: dict) -> str:
@@ -237,24 +237,33 @@ def _should_run(cycle: dict):
237
237
 
238
238
  # only keep inputs that have a positive value
239
239
  inputs = list(filter(lambda i: list_sum(i.get('value', [])) > 0, feed_inputs))
240
- DE = (
241
- get_total_value_converted_with_min_ratio(MODEL, TERM_ID, cycle, inputs, DE_type) if DE_type else None
242
- ) or get_default_digestibility(MODEL, TERM_ID, cycle)
243
- NDF = get_total_value_converted_with_min_ratio(MODEL, TERM_ID, cycle, inputs, 'neutralDetergentFibreContent')
240
+ DE = get_total_value_converted_with_min_ratio(
241
+ MODEL, TERM_ID, cycle, inputs, prop_id=DE_type, is_sum=False
242
+ ) if DE_type else None
243
+ # set as a percentage in the properties
244
+ DE = DE * 100 if DE else DE
245
+ DE_default = get_default_digestibility(MODEL, TERM_ID, cycle)
246
+
247
+ # set as a percentage in the properties
248
+ NDF = get_total_value_converted_with_min_ratio(
249
+ MODEL, TERM_ID, cycle, inputs, prop_id='neutralDetergentFibreContent', is_sum=False
250
+ )
251
+ NDF = NDF * 100 if NDF else NDF
244
252
 
245
253
  enteric_factor = safe_parse_float(_get_lookup_value(
246
- lookup, term, LOOKUPS['liveAnimal'][1], DE, NDF, ionophore, milk_yield
254
+ lookup, term, LOOKUPS['liveAnimal'][1], DE or DE_default, NDF, ionophore, milk_yield
247
255
  ), None)
248
256
  enteric_sd = safe_parse_float(_get_lookup_value(
249
- lookup, term, LOOKUPS['liveAnimal'][2], DE, NDF, ionophore, milk_yield
257
+ lookup, term, LOOKUPS['liveAnimal'][2], DE or DE_default, NDF, ionophore, milk_yield
250
258
  ), None)
251
259
 
252
260
  default_values = _get_default_values(lookup, term)
253
261
 
254
262
  debugValues(cycle, model=MODEL, term=TERM_ID,
255
263
  DE_type=DE_type,
256
- digestibility=DE,
257
- ndf=NDF,
264
+ DE=DE,
265
+ **({'DE_default_lookup': DE_default} if not DE else {}),
266
+ NDF=NDF,
258
267
  ionophore=ionophore,
259
268
  milk_yield=milk_yield,
260
269
  enteric_factor=enteric_factor,
@@ -38,6 +38,7 @@ from hestia_earth.models.utils.time_series import (
38
38
  )
39
39
 
40
40
  from .utils import check_consecutive
41
+ from . import MODEL
41
42
 
42
43
  _ITERATIONS = 10000
43
44
  _MAX_CORRELATION = 1
@@ -424,7 +425,7 @@ def create_should_run_function(
424
425
 
425
426
  land_cover_nodes = get_valid_management_nodes_func(site)
426
427
 
427
- seed = gen_seed(site) # All cycles linked to the same site should be consistent
428
+ seed = gen_seed(site, MODEL, carbon_stock_term_id) # All cycles linked to the same site should be consistent
428
429
  rng = random.default_rng(seed)
429
430
 
430
431
  should_compile_inventory, should_compile_logs = should_compile_inventory_func(
@@ -1315,7 +1316,8 @@ def _format_land_use_inventory(land_use_inventory: dict) -> str:
1315
1316
  """
1316
1317
  KEYS = [
1317
1318
  _InventoryKey.LAND_USE_CHANGE_EVENT,
1318
- _InventoryKey.YEARS_SINCE_LUC_EVENT
1319
+ _InventoryKey.YEARS_SINCE_LUC_EVENT,
1320
+ _InventoryKey.YEARS_SINCE_INVENTORY_START
1319
1321
  ]
1320
1322
 
1321
1323
  inventory_years = sorted(set(non_empty_list(years for years in land_use_inventory.keys())))
@@ -1374,7 +1376,8 @@ def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, Ca
1374
1376
 
1375
1377
  _LAND_USE_INVENTORY_KEY_TO_FORMAT_FUNC = {
1376
1378
  _InventoryKey.LAND_USE_CHANGE_EVENT: _format_bool,
1377
- _InventoryKey.YEARS_SINCE_LUC_EVENT: _format_int
1379
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: _format_int,
1380
+ _InventoryKey.YEARS_SINCE_INVENTORY_START: _format_int
1378
1381
  }
1379
1382
  """
1380
1383
  Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
@@ -1416,7 +1419,7 @@ def create_run_function(
1416
1419
  years_since_inventory_start = data[_InventoryKey.YEARS_SINCE_INVENTORY_START]
1417
1420
 
1418
1421
  is_luc_emission = bool(years_since_luc_event) and years_since_luc_event <= _TRANSITION_PERIOD_YEARS
1419
- is_data_complete = bool(years_since_inventory_start) and years_since_inventory_start > _TRANSITION_PERIOD_YEARS
1422
+ is_data_complete = bool(years_since_inventory_start) and years_since_inventory_start >= _TRANSITION_PERIOD_YEARS
1420
1423
 
1421
1424
  if is_luc_emission:
1422
1425
  # If LUC emission allocate emissions to land use change AND add corresponding zero emission to management
@@ -41,6 +41,7 @@ from .organicCarbonPerHa_utils import (
41
41
  sample_plus_minus_error, sample_plus_minus_uncertainty, SITE_TYPE_TO_IPCC_LAND_USE_CATEGORY,
42
42
  SUPER_MAJORITY_AREA_THRESHOLD, STATS_DEFINITION
43
43
  )
44
+ from . import MODEL
44
45
 
45
46
  _LOOKUPS = {
46
47
  "crop": "IPCC_LAND_USE_CATEGORY",
@@ -555,7 +556,7 @@ def should_run(site: dict) -> tuple[bool, dict, dict]:
555
556
  )
556
557
 
557
558
  kwargs = {
558
- "seed": gen_seed(site),
559
+ "seed": gen_seed(site, MODEL, _TERM_ID),
559
560
  "eco_climate_zone": eco_climate_zone,
560
561
  "ipcc_soil_category": ipcc_soil_category,
561
562
  }
@@ -41,6 +41,7 @@ from .organicCarbonPerHa_utils import (
41
41
  IpccLandUseCategory, IpccManagementCategory, is_cover_crop, MIN_AREA_THRESHOLD, MIN_YIELD_THRESHOLD,
42
42
  sample_constant, sample_plus_minus_uncertainty, sample_truncated_normal, STATS_DEFINITION
43
43
  )
44
+ from . import MODEL
44
45
 
45
46
  _LOOKUPS = {
46
47
  "crop": "IPCC_LAND_USE_CATEGORY",
@@ -62,6 +63,7 @@ _ABOVE_GROUND_CROP_RESIDUE_TOTAL_TERM_ID = "aboveGroundCropResidueTotal"
62
63
  _CARBON_CONTENT_TERM_ID = "carbonContent"
63
64
  _NITROGEN_CONTENT_TERM_ID = "nitrogenContent"
64
65
  _LIGNIN_CONTENT_TERM_ID = "ligninContent"
66
+ _DRY_MATTER_TERM_ID = "dryMatter"
65
67
 
66
68
  _CROP_RESIDUE_MANAGEMENT_TERM_IDS = [
67
69
  "residueIncorporated",
@@ -82,7 +84,8 @@ _DEFAULT_COVER_CROP_BIOMASS = 4000 # TODO: Confirm assumption, Source PAS 2050-
82
84
  _CARBON_INPUT_PROPERTY_TERM_IDS = [
83
85
  _CARBON_CONTENT_TERM_ID,
84
86
  _NITROGEN_CONTENT_TERM_ID,
85
- _LIGNIN_CONTENT_TERM_ID
87
+ _LIGNIN_CONTENT_TERM_ID,
88
+ _DRY_MATTER_TERM_ID
86
89
  ]
87
90
 
88
91
  _CARBON_SOURCE_TERM_TYPES = [
@@ -410,7 +413,7 @@ def should_run(site: dict) -> tuple[bool, dict, dict]:
410
413
  _compile_inventory(cycles, measurement_nodes)
411
414
  if should_compile_inventory else ({}, {})
412
415
  )
413
- kwargs["seed"] = gen_seed(site)
416
+ kwargs["seed"] = gen_seed(site, MODEL, _TERM_ID)
414
417
 
415
418
  valid_years = [year for year, group in inventory.items() if group.get(_InventoryKey.SHOULD_RUN)]
416
419
 
@@ -1380,9 +1383,13 @@ def _calc_carbon_source_ag_crop_residue(node: dict, cycle: dict) -> Union[Carbon
1380
1383
  ])
1381
1384
  mass = value * max(residue_left_on_field, _MIN_RESIDUE_LEFT_ON_FIELD) / 100
1382
1385
 
1386
+ carbon_content, nitrogen_content, lignin_content, dry_matter = _retrieve_carbon_source_properties(node)
1387
+
1383
1388
  carbon_source = CarbonSource(
1384
- mass,
1385
- *_retrieve_carbon_source_properties(node)
1389
+ mass * dry_matter if dry_matter else mass,
1390
+ carbon_content / dry_matter if dry_matter else carbon_content,
1391
+ nitrogen_content / dry_matter if dry_matter else nitrogen_content,
1392
+ lignin_content / dry_matter if dry_matter else lignin_content
1386
1393
  )
1387
1394
 
1388
1395
  return carbon_source if _validate_carbon_source(carbon_source) else None
@@ -1390,7 +1397,7 @@ def _calc_carbon_source_ag_crop_residue(node: dict, cycle: dict) -> Union[Carbon
1390
1397
 
1391
1398
  def _should_run_carbon_source_cover_crop(node: dict) -> bool:
1392
1399
  """
1393
- Determine whether a product is a valid above cover crop carbon source.
1400
+ Determine whether a product is a valid cover crop carbon source.
1394
1401
 
1395
1402
  Parameters
1396
1403
  ----------
@@ -1404,13 +1411,13 @@ def _should_run_carbon_source_cover_crop(node: dict) -> bool:
1404
1411
  Whether the node satisfies the critera.
1405
1412
  """
1406
1413
  LOOKUP = _LOOKUPS["landCover"]
1407
- return all([
1408
- node.get("term", {}).get("termType") in [TermTermType.LANDCOVER.value],
1409
- node_lookup_match(
1410
- node, LOOKUP, IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE[IpccLandUseCategory.ANNUAL_CROPS]
1411
- ),
1412
- is_cover_crop(node)
1413
- ])
1414
+ TARGET_LOOKUP_VALUES = IPCC_LAND_USE_CATEGORY_TO_LAND_COVER_LOOKUP_VALUE[IpccLandUseCategory.ANNUAL_CROPS]
1415
+
1416
+ return (
1417
+ node.get("term", {}).get("termType") in [TermTermType.LANDCOVER.value]
1418
+ and is_cover_crop(node)
1419
+ and node_lookup_match(node, LOOKUP, TARGET_LOOKUP_VALUES)
1420
+ )
1414
1421
 
1415
1422
 
1416
1423
  def _calc_carbon_source_cover_crop(node: dict, *_) -> Union[CarbonSource, None]:
@@ -1475,15 +1482,20 @@ def _calc_carbon_source(node: dict, *_) -> Union[CarbonSource, None]:
1475
1482
  CarbonSource | None
1476
1483
  The carbon source data of the cover crop, or `None` if carbon source data incomplete.
1477
1484
  """
1485
+ mass = get_node_value(node)
1486
+ carbon_content, nitrogen_content, lignin_content, dry_matter = _retrieve_carbon_source_properties(node)
1487
+
1478
1488
  carbon_source = CarbonSource(
1479
- get_node_value(node),
1480
- *_retrieve_carbon_source_properties(node)
1489
+ mass * dry_matter if dry_matter else mass,
1490
+ carbon_content / dry_matter if dry_matter else carbon_content,
1491
+ nitrogen_content / dry_matter if dry_matter else nitrogen_content,
1492
+ lignin_content / dry_matter if dry_matter else lignin_content
1481
1493
  )
1482
1494
 
1483
1495
  return carbon_source if _validate_carbon_source(carbon_source) else None
1484
1496
 
1485
1497
 
1486
- def _retrieve_carbon_source_properties(node: dict) -> tuple[float, float, float]:
1498
+ def _retrieve_carbon_source_properties(node: dict) -> tuple[float, float, float, float]:
1487
1499
  """
1488
1500
  Extract the carbon source properties from an input or product node or, if required, retrieve them from default
1489
1501
  properties.
@@ -1497,12 +1509,11 @@ def _retrieve_carbon_source_properties(node: dict) -> tuple[float, float, float]
1497
1509
  Returns
1498
1510
  -------
1499
1511
  tuple[float, float, float]
1500
- `(carbon_content, nitrogen_content, lignin_content)`
1512
+ `(carbon_content, nitrogen_content, lignin_content, dry_matter)`
1501
1513
  """
1502
- carbon_content, nitrogen_content, lignin_content = (
1514
+ return (
1503
1515
  get_node_property(node, term_id).get("value", 0)/100 for term_id in _CARBON_INPUT_PROPERTY_TERM_IDS
1504
1516
  )
1505
- return carbon_content, nitrogen_content, lignin_content
1506
1517
 
1507
1518
 
1508
1519
  def _validate_carbon_source(carbon_source: CarbonSource) -> bool:
@@ -323,7 +323,14 @@ def calculate_GE(values: list, REM: float, REG: float, NEwool: float, NEm_feed:
323
323
  NEp = _sum_values(values, 'NEp')
324
324
  NEg = _sum_values(values, 'NEg')
325
325
 
326
- return ((NEm + NEa + NEl + NEwork + NEp - NEm_feed)/REM + (NEg + NEwool - NEg_feed)/REG) if all([REM, REG]) else 0
326
+ REM_factor = NEm + NEa + NEl + NEwork + NEp
327
+ REG_factor = NEg + NEwool
328
+
329
+ correction_factor = REM_factor + REG_factor
330
+ NEm_feed_corrected = NEm_feed * REM_factor/correction_factor if correction_factor != 0 else NEm_feed
331
+ NEg_feed_corrected = NEg_feed * REG_factor/correction_factor if correction_factor != 0 else NEg_feed
332
+
333
+ return ((REM_factor - NEm_feed_corrected)/REM + (REG_factor - NEg_feed_corrected)/REG) if all([REM, REG]) else 0
327
334
 
328
335
 
329
336
  def calculate_meanECHHV(practices: list, **log_args) -> float:
@@ -65,7 +65,7 @@ def logShouldRun(log_node: dict, model: str, term: Union[str, None], should_run:
65
65
  def debugMissingLookup(lookup_name: str, row: str, row_value: str, col: str, value, **kwargs):
66
66
  if value is None or value == '':
67
67
  extra = (', ' + _join_args(**kwargs)) if len(kwargs.keys()) > 0 else ''
68
- logger.warn('Missing lookup=%s, %s=%s, column=%s' + extra, lookup_name, row, row_value, col)
68
+ logger.warning('Missing lookup=%s, %s=%s, column=%s' + extra, lookup_name, row, row_value, col)
69
69
 
70
70
 
71
71
  def logErrorRun(model: str, term: str, error: str):