hestia-earth-models 0.75.1__py3-none-any.whl → 0.75.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 (25) hide show
  1. hestia_earth/models/config/Cycle.json +15 -0
  2. hestia_earth/models/cycle/product/economicValueShare.py +4 -4
  3. hestia_earth/models/geospatialDatabase/histosol.py +31 -11
  4. hestia_earth/models/hestia/aboveGroundCropResidueTotal.py +2 -2
  5. hestia_earth/models/hestia/soilClassification.py +31 -13
  6. hestia_earth/models/ipcc2019/animal/pastureGrass.py +3 -1
  7. hestia_earth/models/ipcc2019/burning_utils.py +406 -4
  8. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +26 -8
  9. hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +8 -11
  10. hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +9 -12
  11. hestia_earth/models/ipcc2019/emissionsToAirOrganicSoilBurning.py +516 -0
  12. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +10 -13
  13. hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +56 -433
  14. hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +2 -2
  15. hestia_earth/models/ipcc2019/pastureGrass.py +3 -1
  16. hestia_earth/models/mocking/search-results.json +1 -1
  17. hestia_earth/models/utils/blank_node.py +68 -0
  18. hestia_earth/models/utils/impact_assessment.py +3 -0
  19. hestia_earth/models/version.py +1 -1
  20. hestia_earth/orchestrator/strategies/merge/merge_node.py +32 -2
  21. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/METADATA +1 -1
  22. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/RECORD +25 -24
  23. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/WHEEL +0 -0
  24. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/licenses/LICENSE +0 -0
  25. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,46 @@
1
1
  from enum import Enum
2
+ from functools import reduce
3
+ from itertools import product
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ from typing import Any, Callable, Literal, NotRequired, Optional, TypedDict, Union
7
+ from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition, SiteSiteType
8
+
9
+ from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
10
+ from hestia_earth.utils.lookup import download_lookup, get_table_value, column_name
11
+ from hestia_earth.utils.tools import safe_parse_float
12
+ from hestia_earth.utils.stats import truncated_normal_1d
13
+
14
+ from hestia_earth.models.log import (
15
+ debugMissingLookup, format_bool, format_decimal_percentage, format_float, format_nd_array, format_str, log_as_table,
16
+ logRequirements, logShouldRun
17
+ )
18
+ from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone
19
+ from hestia_earth.models.utils.lookup import get_region_lookup_value
20
+
21
+ from . import MODEL
22
+ from .biomass_utils import BiomassCategory
23
+
24
+ _LOOKUPS = {
25
+ "region-percentageAreaBurnedDuringForestClearance": "percentage_area_burned_during_forest_clearance"
26
+ }
27
+
28
+ ITERATIONS = 10000 # N interations for which the model will run as a Monte Carlo simulation
29
+ TIER = EmissionMethodTier.TIER_1.value
30
+ DEFAULT_FACTOR = {"value": 0}
31
+ _DEFAULT_PERCENT_BURNED = 0
32
+
33
+ _STATS_DEFINITION = EmissionStatsDefinition.SIMULATED.value
34
+
35
+ AMORTISATION_PERIOD = 20 # Emissions should be amortised over 20 years
36
+
37
+ EXCLUDED_ECO_CLIMATE_ZONES = {EcoClimateZone.POLAR_MOIST, EcoClimateZone.POLAR_DRY}
38
+ EXCLUDED_SITE_TYPES = {SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value}
39
+ NATURAL_VEGETATION_CATEGORIES = {
40
+ BiomassCategory.FOREST,
41
+ BiomassCategory.NATURAL_FOREST,
42
+ BiomassCategory.PLANTATION_FOREST
43
+ }
2
44
 
3
45
 
4
46
  class FuelCategory(Enum):
@@ -6,11 +48,10 @@ class FuelCategory(Enum):
6
48
  Natural vegetation fuel categories from IPCC (2019).
7
49
  """
8
50
  BOREAL_FOREST = "boreal-forest"
9
- DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE = "drained-extratropical-organic-soils-wildfire"
51
+ DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE = "drained-extratropical-organic-soils-wildfire" # boreal/temperate
10
52
  DRAINED_TROPICAL_ORGANIC_SOILS_WILDFIRE = "drained-tropical-organic-soils-wildfire"
11
53
  EUCALYPT_FOREST = "eucalypt-forest"
12
54
  NATURAL_TROPICAL_FOREST = "natural-tropical-forest" # mean of primary and secondary tropical forest
13
- PEATLAND_VEGETATION = "peatland-vegetation"
14
55
  PRIMARY_TROPICAL_FOREST = "primary-tropical-forest"
15
56
  SAVANNA_GRASSLAND_EARLY_DRY_SEASON_BURNS = "savanna-grassland-early-dry-season-burns"
16
57
  SAVANNA_GRASSLAND_MID_TO_LATE_DRY_SEASON_BURNS = "savanna-grassland-mid-to-late-dry-season-burns"
@@ -21,17 +62,378 @@ class FuelCategory(Enum):
21
62
  TEMPERATE_FOREST = "temperate-forest"
22
63
  TERTIARY_TROPICAL_FOREST = "tertiary-tropical-forest"
23
64
  TROPICAL_ORGANIC_SOILS_PRESCRIBED_FIRE = "tropical-organic-soils-prescribed-fire"
24
- TUNDRA = "tundra"
25
65
  UNDRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE = "undrained-extratropical-organic-soils-wildfire"
26
66
  UNKNOWN_TROPICAL_FOREST = "unknown-tropical-forest" # mean of primary, secondary and tertiary tropical forest
27
67
 
28
68
 
29
69
  class EmissionCategory(Enum):
30
70
  """
31
- Natural vegetation burning emission categories from IPCC (2019).
71
+ Natural vegetation and organic soil burning emission categories from IPCC (2019).
32
72
  """
33
73
  AGRICULTURAL_RESIDUES = "agricultural-residues"
34
74
  BIOFUEL_BURNING = "biofuel-burning"
75
+ EXTRATROPICAL_ORGANIC_SOILS = "extratropical-organic-soils"
35
76
  OTHER_FOREST = "other-forest"
36
77
  SAVANNA_AND_GRASSLAND = "savanna-and-grassland"
37
78
  TROPICAL_FOREST = "tropical-forest"
79
+ TROPICAL_ORGANIC_SOILS = "tropical-organic-soils"
80
+
81
+
82
+ class InventoryYear(TypedDict, total=False):
83
+ biomass_category_summary: dict[BiomassCategory, float]
84
+ natural_vegetation_delta: dict[BiomassCategory, float]
85
+ fuel_burnt_per_category: dict[FuelCategory, npt.NDArray]
86
+ annual_emissions: dict[str, npt.NDArray]
87
+ amortised_emissions: dict[str, npt.NDArray]
88
+ share_of_emissions: dict[str, float] # {cycle_id (str): value, ...}
89
+ allocated_emissions: dict[str, dict[str, npt.NDArray]]
90
+ percent_organic_soils: NotRequired[float]
91
+
92
+
93
+ InventoryKey = Literal[
94
+ "biomass_category_summary",
95
+ "natural_vegetation_delta",
96
+ "fuel_burnt_per_category",
97
+ "annual_emissions",
98
+ "amortised_emissions",
99
+ "share_of_emissions",
100
+ "allocated_emissions",
101
+ "percent_organic_soils"
102
+ ]
103
+
104
+ Inventory = dict[int, InventoryYear]
105
+ """
106
+ {year (int): data (_InventoryYear)}
107
+ """
108
+
109
+
110
+ _FUEL_CATEGORY_TO_EMISSION_CATEGORY = {
111
+ FuelCategory.BOREAL_FOREST: EmissionCategory.OTHER_FOREST,
112
+ FuelCategory.EUCALYPT_FOREST: EmissionCategory.OTHER_FOREST,
113
+ FuelCategory.NATURAL_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
114
+ FuelCategory.PRIMARY_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
115
+ FuelCategory.SAVANNA_GRASSLAND_EARLY_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
116
+ FuelCategory.SAVANNA_GRASSLAND_MID_TO_LATE_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
117
+ FuelCategory.SAVANNA_WOODLAND_EARLY_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
118
+ FuelCategory.SAVANNA_WOODLAND_MID_TO_LATE_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
119
+ FuelCategory.SECONDARY_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
120
+ FuelCategory.SHRUBLAND: EmissionCategory.SAVANNA_AND_GRASSLAND,
121
+ FuelCategory.TEMPERATE_FOREST: EmissionCategory.OTHER_FOREST,
122
+ FuelCategory.TERTIARY_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
123
+ FuelCategory.UNKNOWN_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
124
+ FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE: EmissionCategory.EXTRATROPICAL_ORGANIC_SOILS,
125
+ FuelCategory.DRAINED_TROPICAL_ORGANIC_SOILS_WILDFIRE: EmissionCategory.TROPICAL_ORGANIC_SOILS,
126
+ FuelCategory.TROPICAL_ORGANIC_SOILS_PRESCRIBED_FIRE: EmissionCategory.TROPICAL_ORGANIC_SOILS,
127
+ FuelCategory.UNDRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE: EmissionCategory.EXTRATROPICAL_ORGANIC_SOILS
128
+ }
129
+ """
130
+ Mapping from natural vegetation and organic soil fuel category to emission category.
131
+ """
132
+
133
+
134
+ def get_emission_category(fuel_category: FuelCategory) -> EmissionCategory:
135
+ """
136
+ Get the IPCC (2019) emission category that corresponds to a fuel category.
137
+ """
138
+ return _FUEL_CATEGORY_TO_EMISSION_CATEGORY.get(fuel_category)
139
+
140
+
141
+ def _sample_truncated_normal(
142
+ *, iterations: int, value: float, sd: float, seed: Union[int, np.random.Generator, None] = None, **_
143
+ ) -> npt.NDArray:
144
+ """
145
+ Randomly sample a model parameter with a truncated normal distribution. Neither fuel factors nor emission factors
146
+ can be below 0, so truncated normal sampling used.
147
+ """
148
+ return truncated_normal_1d(shape=(1, iterations), mu=value, sigma=sd, low=0, high=np.inf, seed=seed)
149
+
150
+
151
+ def _sample_constant(*, value: float, **_) -> npt.NDArray:
152
+ """Sample a constant model parameter."""
153
+ return np.array(value)
154
+
155
+
156
+ _KWARGS_TO_SAMPLE_FUNC = {
157
+ # ("value", "se", "n"): _sample_standard_error_normal,
158
+ ("value", "sd"): _sample_truncated_normal,
159
+ ("value",): _sample_constant
160
+ }
161
+ """
162
+ Mapping from available distribution data to sample function.
163
+ """
164
+
165
+
166
+ def get_sample_func(kwargs: dict) -> Callable:
167
+ """
168
+ Select the correct sample function for a parameter based on the distribution data available. All possible
169
+ parameters for the model should have, at a minimum, a `value`, meaning that no default function needs to be
170
+ specified.
171
+
172
+ This function has been extracted into it's own method to allow for mocking of sample function.
173
+
174
+ Keyword Args
175
+ ------------
176
+ value : float
177
+ The distribution mean.
178
+ sd : float
179
+ The standard deviation of the distribution.
180
+ se : float
181
+ The standard error of the distribution.
182
+ n : float
183
+ Sample size.
184
+
185
+ Returns
186
+ -------
187
+ Callable
188
+ The sample function for the distribution.
189
+ """
190
+ return next(
191
+ sample_func for required_kwargs, sample_func in _KWARGS_TO_SAMPLE_FUNC.items()
192
+ if all(kwarg in kwargs.keys() for kwarg in required_kwargs)
193
+ )
194
+
195
+
196
+ def _get_fuel_factor(fuel_category: FuelCategory, emission_term_ids: list[str]) -> dict:
197
+ """
198
+ Retrieve distribution data for a specific fuel category.
199
+ """
200
+ LOOKUP_KEY = "ipcc2019FuelCategory_tonnesDryMatterCombustedPerHaBurned"
201
+ LOOKUP_FILENAME = f"{LOOKUP_KEY}.csv"
202
+ TARGET_DATA = (
203
+ "value",
204
+ # "se", # useless without n data
205
+ # "n" # TODO: add n data to lookup
206
+ )
207
+
208
+ row = fuel_category.name
209
+
210
+ lookup = download_lookup(LOOKUP_FILENAME)
211
+
212
+ data = {
213
+ target: get_table_value(lookup, column_name("FuelCategory"), row, column_name(target))
214
+ for target in TARGET_DATA
215
+ }
216
+
217
+ for term_id, target in product(emission_term_ids, TARGET_DATA):
218
+ debugMissingLookup(LOOKUP_FILENAME, "FuelCategory", row, target, data.get(target), model=MODEL, term=term_id)
219
+
220
+ return (
221
+ {
222
+ k: parsed for k, v in data.items() if (parsed := safe_parse_float(v, default=None)) is not None
223
+ } # remove missing
224
+ or DEFAULT_FACTOR # if parsed dict empty, return default
225
+ )
226
+
227
+
228
+ def sample_fuel_factor(
229
+ fuel_category: FuelCategory,
230
+ emission_term_ids: list[str],
231
+ *,
232
+ seed: Union[int, np.random.Generator, None] = None
233
+ ) -> npt.NDArray:
234
+ """
235
+ Generate random samples from a fuel factor's distribution data.
236
+ """
237
+ factor_data = _get_fuel_factor(fuel_category, emission_term_ids)
238
+ sample_func = get_sample_func(factor_data)
239
+ return sample_func(iterations=ITERATIONS, seed=seed, **factor_data)
240
+
241
+
242
+ def get_percent_burned(site: str):
243
+ LOOKUP_KEY = "region-percentageAreaBurnedDuringForestClearance"
244
+ LOOKUP_FILENAME = f"{LOOKUP_KEY}.csv"
245
+ country_id = site.get("country", {}).get("@id")
246
+
247
+ value = get_region_lookup_value(LOOKUP_FILENAME, country_id, _LOOKUPS[LOOKUP_KEY])
248
+ return safe_parse_float(value, _DEFAULT_PERCENT_BURNED)
249
+
250
+
251
+ def _sum_cycle_emissions(term_id: str, cycle_id: str, inventory: Inventory) -> npt.NDArray:
252
+ """
253
+ Sum the emissions allocated to a cycle.
254
+ """
255
+ KEY = "allocated_emissions"
256
+
257
+ def add_cycle_emissions(result: npt.NDArray, year: int) -> npt.NDArray:
258
+ allocated_emissions = inventory.get(year, {}).get(KEY, {}).get(term_id, {})
259
+ return result + allocated_emissions.get(cycle_id, np.array(0))
260
+
261
+ return reduce(add_cycle_emissions, inventory.keys(), np.array(0))
262
+
263
+
264
+ def calc_emission(fuel_burnt: npt.NDArray, emission_factor: npt.NDArray, conversion_factor: float = 1) -> npt.NDArray:
265
+ """
266
+ Calculate the emission from a fuel burning.
267
+
268
+ Parameters
269
+ ----------
270
+ fuel_burnt : NDArray
271
+ The mass of burnt fuel (kg).
272
+ emission_factor : NDArray
273
+ Emission conversion factor (kg emission per kg of fuel burnt).
274
+ conversion_factor : float, optional
275
+ Optional factor to convert emission factor to other units (e.g., from CO2-C to CO2).
276
+
277
+ Returns
278
+ -------
279
+ NDArray
280
+ The mass of emission (kg)
281
+ """
282
+ return fuel_burnt * emission_factor * conversion_factor
283
+
284
+
285
+ def run_emission(term_id: str, cycle_id: str, inventory: Inventory) -> list[dict]:
286
+ """
287
+ Retrieve the sum relevant emissions and format them as a HESTIA
288
+ [Emission node](https://www.hestia.earth/schema/Emission).
289
+ """
290
+ emission = _sum_cycle_emissions(term_id, cycle_id, inventory)
291
+ kwargs = (
292
+ calc_descriptive_stats(emission, _STATS_DEFINITION) if emission.size > 1
293
+ else {"value": [emission]}
294
+ )
295
+ return term_id, kwargs
296
+
297
+
298
+ def _format_column_header(*keys: tuple[Union[Enum, str], ...]) -> str:
299
+ """Format a variable number of enums and strings for logging as a table column header."""
300
+ return " ".join(format_str(k.value if isinstance(k, Enum) else format_str(k)) for k in keys)
301
+
302
+
303
+ def _format_eco_climate_zone(value: EcoClimateZone) -> str:
304
+ """Format an eco-climate zone for logging."""
305
+ return (
306
+ format_str(str(value.name).lower().replace("_", " ").capitalize()) if isinstance(value, EcoClimateZone)
307
+ else "None"
308
+ )
309
+
310
+
311
+ _LOGS_FORMAT_DATA: dict[str, Callable] = {
312
+ "has_valid_site_type": format_bool,
313
+ "eco_climate_zone": _format_eco_climate_zone,
314
+ "has_valid_eco_climate_zone": format_bool,
315
+ "has_land_cover_nodes": format_bool,
316
+ "should_compile_inventory": format_bool,
317
+ "percent_burned": lambda x: format_float(x, "pct"),
318
+ }
319
+ _DEFAULT_FORMAT_FUNC = format_str
320
+
321
+
322
+ def _format_logs(logs: dict) -> dict[str, str]:
323
+ """
324
+ Format model logs - excluding the inventory data, which must be formatted separately.
325
+ """
326
+ return {key: _LOGS_FORMAT_DATA.get(key, _DEFAULT_FORMAT_FUNC)(value) for key, value in logs.items()}
327
+
328
+
329
+ _INVENTORY_FORMAT_DATA: dict[InventoryKey, dict[Literal["filter_by", "format_func"], Any]] = {
330
+ "fuel_burnt_per_category": {
331
+ "format_func": lambda x: format_nd_array(x, "kg")
332
+ },
333
+ "annual_emissions": {
334
+ "filter_by": ("term_id", ),
335
+ "format_func": lambda x: format_nd_array(x, "kg")
336
+ },
337
+ "amortised_emissions": {
338
+ "filter_by": ("term_id", ),
339
+ "format_func": lambda x: format_nd_array(x, "kg")
340
+ },
341
+ "share_of_emissions": {
342
+ "filter_by": ("cycle_id", ),
343
+ "format_func": format_decimal_percentage
344
+ },
345
+ "allocated_emissions": {
346
+ "filter_by": ("term_id", "cycle_id"),
347
+ "format_func": lambda x: format_nd_array(x, "kg")
348
+ },
349
+ "percent_organic_soils": {
350
+ "format_func": lambda x: format_float(x, "pct")
351
+ }
352
+ }
353
+ """
354
+ Mapping between inventory key and formatting options for logging in a table. Inventory keys not included in the dict
355
+ will not be logged in the table.
356
+ """
357
+
358
+
359
+ def _flatten_dict(nested_dict: dict) -> dict[tuple, Any]:
360
+ """
361
+ Flatten a nested dict, returns dict with keys as tuples with format `(key_level_1, key_level_2, ..., key_level_n)`.
362
+ """
363
+ def flatten(current: dict, path: tuple = ()):
364
+
365
+ if isinstance(current, dict):
366
+ for key, value in current.items():
367
+ yield from flatten(value, path + (key,))
368
+ else:
369
+ yield (path, current)
370
+
371
+ return dict(flatten(nested_dict))
372
+
373
+
374
+ def _get_relevant_inner_keys(
375
+ term_id: str,
376
+ cycle_id: str,
377
+ key: str,
378
+ inventory: Inventory,
379
+ *,
380
+ filter_by: Optional[tuple[Literal["term_id", "cycle_id"], ...]] = None,
381
+ **_
382
+ ) -> list[tuple]:
383
+ """
384
+ Get the column headings for the formatted table. Nested inventory values should be flattened, with nested keys
385
+ being transformed into a tuple with the format `(key_level_1, key_level_2, ..., key_level_n)`.
386
+
387
+ Inner keys not relevant to the emission term being logged or the cycle the model is running on should be excluded.
388
+ """
389
+ FILTER_VALUES = {"term_id": term_id, "cycle_id": cycle_id}
390
+ filter_target = (
391
+ tuple(val for f in filter_by if (val := FILTER_VALUES.get(f)))
392
+ if filter_by else None
393
+ )
394
+
395
+ inner_keys = {
396
+ tuple(k) for inner in inventory.values() for k in _flatten_dict(inner.get(key, {}))
397
+ if not filter_target or k == filter_target
398
+ }
399
+
400
+ return sorted(
401
+ inner_keys,
402
+ key=lambda category: category.value if isinstance(category, Enum) else str(category)
403
+ )
404
+
405
+
406
+ def _format_inventory(term_id: str, cycle_id: str, inventory: dict) -> str:
407
+ """
408
+ Format the inventory for logging as a table.
409
+
410
+ Extract relevant data, flatten nested dicts and format inventory values based on expected data type.
411
+ """
412
+ relevant_inventory_keys = {
413
+ inventory_key: _get_relevant_inner_keys(term_id, cycle_id, inventory_key, inventory, **kwargs)
414
+ for inventory_key, kwargs in _INVENTORY_FORMAT_DATA.items()
415
+ }
416
+
417
+ return log_as_table(
418
+ {
419
+ "year": year,
420
+ **{
421
+ _format_column_header(inventory_key, *inner_key): _INVENTORY_FORMAT_DATA[inventory_key]["format_func"](
422
+ reduce(lambda d, k: d.get(k, {}), [year, inventory_key, *inner_key], inventory)
423
+ )
424
+ for inventory_key, relevant_inner_keys in relevant_inventory_keys.items()
425
+ for inner_key in relevant_inner_keys
426
+ }
427
+ } for year in inventory
428
+ ) if inventory else "None"
429
+
430
+
431
+ def log_emission_data(should_run: bool, term_id: str, cycle: dict, inventory: dict, logs: dict):
432
+ """
433
+ Format and log the model logs and inventory.
434
+ """
435
+ formatted_logs = _format_logs(logs)
436
+ formatted_inventory = _format_inventory(term_id, cycle.get("@id"), inventory)
437
+
438
+ logRequirements(cycle, model=MODEL, term=term_id, **formatted_logs, inventory=formatted_inventory)
439
+ logShouldRun(cycle, MODEL, term_id, should_run, methodTier=TIER)
@@ -12,13 +12,20 @@ from hestia_earth.models.utils.emission import _new_emission
12
12
  from hestia_earth.models.utils.measurement import most_relevant_measurement_value
13
13
  from hestia_earth.models.utils.input import total_excreta
14
14
  from hestia_earth.models.utils.lookup import get_region_lookup_value
15
+ from hestia_earth.models.utils.property import get_node_property
15
16
  from . import MODEL
16
17
 
17
18
  REQUIREMENTS = {
18
19
  "Cycle": {
19
20
  "cycleDuration": "",
20
21
  "endDate": "",
21
- "practices": [{"@type": "Practice", "value": "", "term.termType": "excretaManagement"}],
22
+ "practices": [{
23
+ "@type": "Practice",
24
+ "term.termType": "excretaManagement",
25
+ "optional": {
26
+ "properties": [{"@type": "Property", "term.id": "methaneConversionFactor"}]
27
+ }
28
+ }],
22
29
  "site": {
23
30
  "@type": "Site",
24
31
  "country": {"@type": "Term", "termType": "region"},
@@ -39,6 +46,7 @@ RETURNS = {
39
46
  }
40
47
  TERM_ID = 'ch4ToAirExcreta'
41
48
  TIER = EmissionMethodTier.TIER_2.value
49
+ _CONV_FACTOR_PROP_ID = REQUIREMENTS['Cycle']['practices'][0]['optional']['properties'][0]['term.id']
42
50
 
43
51
 
44
52
  class DURATION(Enum):
@@ -110,17 +118,27 @@ def _get_ch4_conv_factor(cycle: dict):
110
118
  measurements = cycle.get('site', {}).get('measurements', [])
111
119
  ecoClimateZone = most_relevant_measurement_value(measurements, 'ecoClimateZone', end_date)
112
120
  practices = filter_list_term_type(cycle.get('practices', []), TermTermType.EXCRETAMANAGEMENT)
113
- practice_id = practices[0].get('term', {}).get('@id') if len(practices) > 0 else None
121
+ primary_practice = practices[0] if len(practices) > 0 else {}
122
+ practice_id = primary_practice.get('term', {}).get('@id')
114
123
 
115
124
  debugValues(cycle, model=MODEL, term=TERM_ID,
116
125
  duration=duration_key.value,
117
126
  ecoClimateZone=ecoClimateZone,
118
- practice_id=practice_id)
119
-
120
- return _get_excretaManagement_MCF_from_lookup(practice_id, ecoClimateZone, duration_key) if all([
121
- practice_id,
122
- ecoClimateZone is not None
123
- ]) else None
127
+ excreta_management_practice_id=practice_id)
128
+
129
+ practive_ch4_conv_factor = get_node_property(
130
+ node=primary_practice,
131
+ property=_CONV_FACTOR_PROP_ID,
132
+ find_default_property=False,
133
+ download_from_hestia=False
134
+ ).get('value')
135
+
136
+ return practive_ch4_conv_factor or (
137
+ _get_excretaManagement_MCF_from_lookup(practice_id, ecoClimateZone, duration_key) if all([
138
+ practice_id,
139
+ ecoClimateZone is not None
140
+ ]) else None
141
+ )
124
142
 
125
143
 
126
144
  def _should_run(cycle: dict):
@@ -42,7 +42,7 @@ REQUIREMENTS = {
42
42
  "site": {
43
43
  "@type": "Site",
44
44
  "measurements": [
45
- {"@type": "Measurement", "value": "", "term.@id": "histosol"},
45
+ {"@type": "Measurement", "value": "", "term.@id": "organicSoils"},
46
46
  {"@type": "Measurement", "value": "", "term.@id": "ecoClimateZone"}
47
47
  ]
48
48
  },
@@ -195,10 +195,7 @@ def _should_run(cycle: dict):
195
195
  seed = gen_seed(cycle, MODEL, TERM_ID)
196
196
  rng = np.random.default_rng(seed)
197
197
 
198
- def _get_measurement_content(term_id: str):
199
- return most_relevant_measurement_value(measurements, term_id, end_date)
200
-
201
- histosol = _get_measurement_content('histosol')
198
+ organic_soils = most_relevant_measurement_value(measurements, "organicSoils", end_date)
202
199
  eco_climate_zone = get_eco_climate_zone_value(cycle, as_enum=True)
203
200
  organic_soil_category = assign_organic_soil_category(cycle, log_id=TERM_ID)
204
201
  ditch_category = assign_ditch_category(cycle)
@@ -227,7 +224,7 @@ def _should_run(cycle: dict):
227
224
  ditch_factor=format_nd_array(ditch_factor),
228
225
  ditch_frac=format_nd_array(ditch_frac),
229
226
  land_occupation=format_float(land_occupation),
230
- histosol=format_float(histosol)
227
+ organic_soils=format_float(organic_soils)
231
228
  )
232
229
 
233
230
  should_run = all([
@@ -239,25 +236,25 @@ def _should_run(cycle: dict):
239
236
  ditch_factor,
240
237
  ditch_frac,
241
238
  land_occupation,
242
- histosol
239
+ organic_soils
243
240
  ]
244
241
  )
245
242
  ])
246
243
 
247
244
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
248
245
 
249
- return should_run, emission_factor, ditch_factor, ditch_frac, histosol, land_occupation
246
+ return should_run, emission_factor, ditch_factor, ditch_frac, organic_soils, land_occupation
250
247
 
251
248
 
252
249
  def _run(
253
250
  emission_factor: npt.NDArray,
254
251
  ditch_factor: npt.NDArray,
255
252
  ditch_frac: npt.NDArray,
256
- histosol: float,
253
+ organic_soils: float,
257
254
  land_occupation: float
258
255
  ):
259
- land_emission = calc_emission(TERM_ID, emission_factor, histosol, land_occupation)
260
- ditch_emission = calc_emission(TERM_ID, ditch_factor, histosol, land_occupation)
256
+ land_emission = calc_emission(TERM_ID, emission_factor, organic_soils, land_occupation)
257
+ ditch_emission = calc_emission(TERM_ID, ditch_factor, organic_soils, land_occupation)
261
258
 
262
259
  result = (ditch_emission * ditch_frac) + (land_emission * (1 - ditch_frac))
263
260
  descriptive_stats = calc_descriptive_stats(result, _STATS_DEFINITION)
@@ -39,7 +39,7 @@ REQUIREMENTS = {
39
39
  "site": {
40
40
  "@type": "Site",
41
41
  "measurements": [
42
- {"@type": "Measurement", "value": "", "term.@id": "histosol"},
42
+ {"@type": "Measurement", "value": "", "term.@id": "organicSoils"},
43
43
  {"@type": "Measurement", "value": "", "term.@id": "ecoClimateZone"}
44
44
  ]
45
45
  },
@@ -164,10 +164,7 @@ def _should_run(cycle: dict):
164
164
  seed = gen_seed(cycle, MODEL, TERM_ID)
165
165
  rng = np.random.default_rng(seed)
166
166
 
167
- def _get_measurement_content(term_id: str):
168
- return most_relevant_measurement_value(measurements, term_id, end_date)
169
-
170
- histosol = _get_measurement_content('histosol')
167
+ organic_soils = most_relevant_measurement_value(measurements, "organicSoils", end_date)
171
168
  eco_climate_zone = get_eco_climate_zone_value(cycle, as_enum=True)
172
169
  organic_soil_category = assign_organic_soil_category(cycle, log_id=TERM_ID)
173
170
 
@@ -183,7 +180,7 @@ def _should_run(cycle: dict):
183
180
  organic_soil_category=organic_soil_category,
184
181
  emission_factor=format_nd_array(emission_factor),
185
182
  land_occupation=format_float(land_occupation),
186
- histosol=format_float(histosol)
183
+ organic_soils=format_float(organic_soils)
187
184
  )
188
185
 
189
186
  should_run = all([
@@ -193,22 +190,22 @@ def _should_run(cycle: dict):
193
190
  var is not None for var in [
194
191
  emission_factor,
195
192
  land_occupation,
196
- histosol
193
+ organic_soils
197
194
  ]
198
195
  )
199
196
  ])
200
197
 
201
198
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
202
199
 
203
- return should_run, emission_factor, histosol, land_occupation
200
+ return should_run, emission_factor, organic_soils, land_occupation
204
201
 
205
202
 
206
- def _run(emission_factor: npt.NDArray, histosol: float, land_occupation: float):
207
- result = calc_emission(TERM_ID, emission_factor, histosol, land_occupation)
203
+ def _run(emission_factor: npt.NDArray, organic_soils: float, land_occupation: float):
204
+ result = calc_emission(TERM_ID, emission_factor, organic_soils, land_occupation)
208
205
  descriptive_stats = calc_descriptive_stats(result, _STATS_DEFINITION)
209
206
  return [_emission(descriptive_stats)]
210
207
 
211
208
 
212
209
  def run(cycle: dict):
213
- should_run, emission_factor, histosol, land_occupation = _should_run(cycle)
214
- return _run(emission_factor, histosol, land_occupation) if should_run else []
210
+ should_run, emission_factor, organic_soils, land_occupation = _should_run(cycle)
211
+ return _run(emission_factor, organic_soils, land_occupation) if should_run else []