hestia-earth-models 0.75.1__py3-none-any.whl → 0.75.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 (42) hide show
  1. hestia_earth/models/config/Cycle.json +183 -16
  2. hestia_earth/models/cycle/product/economicValueShare.py +4 -4
  3. hestia_earth/models/emepEea2019/blackCarbonToAirFuelCombustion.py +33 -0
  4. hestia_earth/models/emepEea2019/ch4ToAirFuelCombustion.py +33 -0
  5. hestia_earth/models/emepEea2019/coToAirFuelCombustion.py +33 -0
  6. hestia_earth/models/emepEea2019/nmvocToAirFuelCombustion.py +33 -0
  7. hestia_earth/models/emepEea2019/pm10ToAirFuelCombustion.py +33 -0
  8. hestia_earth/models/emepEea2019/pm25ToAirFuelCombustion.py +33 -0
  9. hestia_earth/models/emepEea2019/tspToAirFuelCombustion.py +33 -0
  10. hestia_earth/models/faostat2018/seed.py +9 -8
  11. hestia_earth/models/geospatialDatabase/histosol.py +31 -11
  12. hestia_earth/models/hestia/aboveGroundCropResidueTotal.py +2 -2
  13. hestia_earth/models/hestia/management.py +5 -4
  14. hestia_earth/models/hestia/soilClassification.py +31 -13
  15. hestia_earth/models/ipcc2019/animal/pastureGrass.py +19 -11
  16. hestia_earth/models/ipcc2019/burning_utils.py +406 -4
  17. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +26 -8
  18. hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +8 -11
  19. hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +9 -12
  20. hestia_earth/models/ipcc2019/emissionsToAirOrganicSoilBurning.py +516 -0
  21. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +10 -13
  22. hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +56 -433
  23. hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +2 -2
  24. hestia_earth/models/ipcc2019/pastureGrass.py +19 -11
  25. hestia_earth/models/ipcc2019/pastureGrass_utils.py +17 -10
  26. hestia_earth/models/linkedImpactAssessment/emissions.py +1 -1
  27. hestia_earth/models/mocking/search-results.json +1 -1
  28. hestia_earth/models/pefcrGuidanceDocument2017/__init__.py +13 -0
  29. hestia_earth/models/pefcrGuidanceDocument2017/pesticideToAirPesticideApplication.py +29 -0
  30. hestia_earth/models/pefcrGuidanceDocument2017/pesticideToSoilPesticideApplication.py +29 -0
  31. hestia_earth/models/pefcrGuidanceDocument2017/pesticideToWaterPesticideApplication.py +29 -0
  32. hestia_earth/models/pefcrGuidanceDocument2017/utils.py +55 -0
  33. hestia_earth/models/pooreNemecek2018/saplingsDepreciatedAmountPerCycle.py +1 -1
  34. hestia_earth/models/utils/blank_node.py +68 -0
  35. hestia_earth/models/utils/impact_assessment.py +3 -0
  36. hestia_earth/models/version.py +1 -1
  37. hestia_earth/orchestrator/strategies/merge/merge_node.py +32 -2
  38. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.3.dist-info}/METADATA +1 -1
  39. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.3.dist-info}/RECORD +42 -29
  40. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.3.dist-info}/WHEEL +0 -0
  41. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.3.dist-info}/licenses/LICENSE +0 -0
  42. {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.3.dist-info}/top_level.txt +0 -0
@@ -1,30 +1,25 @@
1
- from enum import Enum
2
1
  from functools import lru_cache, reduce
3
- from itertools import product
4
2
  import numpy as np
5
3
  import numpy.typing as npt
6
- from typing import Any, Callable, Literal, Optional, TypedDict, Union
7
- from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition, SiteSiteType
8
- from hestia_earth.utils.lookup import download_lookup, get_table_value
4
+ from typing import Callable, Union
5
+
6
+ from hestia_earth.utils.stats import gen_seed
9
7
  from hestia_earth.utils.tools import safe_parse_float
10
- from hestia_earth.utils.stats import gen_seed, repeat_single, truncated_normal_1d
11
- from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
12
8
 
13
- from hestia_earth.models.log import (
14
- debugMissingLookup, format_bool, format_decimal_percentage, format_float, format_nd_array, format_str, log_as_table,
15
- logRequirements, logShouldRun
16
- )
9
+ from hestia_earth.models.utils import split_on_condition
17
10
  from hestia_earth.models.utils.blank_node import group_nodes_by_year
18
11
  from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
19
12
  from hestia_earth.models.utils.emission import _new_emission
20
- from hestia_earth.models.utils.lookup import get_region_lookup_value
21
13
  from hestia_earth.models.utils.site import related_cycles
22
14
  from hestia_earth.models.utils.term import get_lookup_value
23
15
 
24
16
  from . import MODEL
25
17
  from .biomass_utils import BiomassCategory, get_valid_management_nodes, summarise_land_cover_nodes
26
- from .burning_utils import EmissionCategory, FuelCategory
27
-
18
+ from .burning_utils import (
19
+ calc_emission, DEFAULT_FACTOR, EmissionCategory, EXCLUDED_ECO_CLIMATE_ZONES, EXCLUDED_SITE_TYPES, FuelCategory,
20
+ ITERATIONS, get_emission_category, get_percent_burned, get_sample_func, Inventory, InventoryYear,
21
+ NATURAL_VEGETATION_CATEGORIES, run_emission, sample_fuel_factor, AMORTISATION_PERIOD, log_emission_data, TIER
22
+ )
28
23
 
29
24
  REQUIREMENTS = {
30
25
  "Cycle": {
@@ -86,59 +81,9 @@ RETURNS = {
86
81
  "methodClassification": "tier 1 model"
87
82
  }]
88
83
  }
89
- TERM_ID = 'ch4ToAirNaturalVegetationBurning,coToAirNaturalVegetationBurning,n2OToAirNaturalVegetationBurningDirect,noxToAirNaturalVegetationBurning' # noqa: E501
90
-
91
- EMISSION_TERM_IDS = TERM_ID.split(",")
92
- TIER = EmissionMethodTier.TIER_1.value
93
- STATS_DEFINITION = EmissionStatsDefinition.SIMULATED.value
94
-
95
- _ITERATIONS = 10000 # N interations for which the model will run as a Monte Carlo simulation
96
- _AMORTISATION_PERIOD = 20 # Emissions should be amortised over 20 years
97
-
98
- _EXCLUDED_ECO_CLIMATE_ZONES = {EcoClimateZone.POLAR_MOIST, EcoClimateZone.POLAR_DRY}
99
- _EXCLUDED_SITE_TYPES = {SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value}
100
- _NATURAL_VEGETATION_CATEGORIES = {
101
- BiomassCategory.FOREST,
102
- BiomassCategory.NATURAL_FOREST,
103
- BiomassCategory.PLANTATION_FOREST
104
- }
105
- _DEFAULT_FACTOR = {"value": 0}
106
- _DEFAULT_PERCENT_BURNED = 0
107
-
108
-
109
- _EmissionTermId = Literal[
110
- "ch4ToAirNaturalVegetationBurning",
111
- "coToAirNaturalVegetationBurning",
112
- "n2OToAirNaturalVegetationBurningDirect",
113
- "noxToAirNaturalVegetationBurning"
114
- ]
115
-
116
-
117
- class _InventoryYear(TypedDict, total=False):
118
- biomass_category_summary: dict[BiomassCategory, float]
119
- natural_vegetation_delta: dict[BiomassCategory, float]
120
- fuel_burnt_per_category: dict[FuelCategory, npt.NDArray]
121
- annual_emissions: dict[_EmissionTermId, npt.NDArray]
122
- amortised_emissions: dict[_EmissionTermId, npt.NDArray]
123
- share_of_emissions: dict[str, float] # {cycle_id (str): value, ...}
124
- allocated_emissions: dict[_EmissionTermId, dict[str, npt.NDArray]]
125
-
126
-
127
- _InventoryKey = Literal[
128
- "biomass_category_summary",
129
- "natural_vegetation_delta",
130
- "fuel_burnt_per_category",
131
- "annual_emissions",
132
- "amortised_emissions",
133
- "share_of_emissions",
134
- "allocated_emissions"
135
- ]
136
-
137
- _Inventory = dict[int, _InventoryYear]
138
- """
139
- {year (int): data (_InventoryYear)}
140
- """
84
+ TERM_ID = 'ch4ToAirNaturalVegetationBurning,coToAirNaturalVegetationBurning,n2OToAirNaturalVegetationBurningDirect,nh3ToAirNaturalVegetationBurning,noxToAirNaturalVegetationBurning' # noqa: E501
141
85
 
86
+ _EMISSION_TERM_IDS = TERM_ID.split(",")
142
87
 
143
88
  _BIOMASS_CATEGORY_TO_FUEL_CATEGORY = {
144
89
  BiomassCategory.FOREST: {
@@ -182,24 +127,17 @@ _BIOMASS_CATEGORY_TO_FUEL_CATEGORY = {
182
127
  Mapping from IPCC biomass category and eco-climate zone to natural vegetation fuel category.
183
128
  """
184
129
 
185
- _FUEL_CATEGORY_TO_EMISSION_CATEGORY = {
186
- FuelCategory.BOREAL_FOREST: EmissionCategory.OTHER_FOREST,
187
- FuelCategory.EUCALYPT_FOREST: EmissionCategory.OTHER_FOREST,
188
- FuelCategory.NATURAL_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
189
- FuelCategory.PRIMARY_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
190
- FuelCategory.SAVANNA_GRASSLAND_EARLY_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
191
- FuelCategory.SAVANNA_GRASSLAND_MID_TO_LATE_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
192
- FuelCategory.SAVANNA_WOODLAND_EARLY_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
193
- FuelCategory.SAVANNA_WOODLAND_MID_TO_LATE_DRY_SEASON_BURNS: EmissionCategory.SAVANNA_AND_GRASSLAND,
194
- FuelCategory.SECONDARY_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
195
- FuelCategory.SHRUBLAND: EmissionCategory.SAVANNA_AND_GRASSLAND,
196
- FuelCategory.TEMPERATE_FOREST: EmissionCategory.OTHER_FOREST,
197
- FuelCategory.TERTIARY_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST,
198
- FuelCategory.UNKNOWN_TROPICAL_FOREST: EmissionCategory.TROPICAL_FOREST
199
- }
200
- """
201
- Mapping from natural vegetation fuel category to natural vegetation burning emission category.
202
- """
130
+
131
+ def _emission(term_id: str, kwargs) -> dict:
132
+ """
133
+ Build a HESTIA [Emission node](https://www.hestia.earth/schema/Emission) using model output data.
134
+ """
135
+ value_keys, other_keys = split_on_condition([*kwargs], lambda k: k in ("value", "sd", "min", "max"))
136
+ emission = _new_emission(term=term_id, model=MODEL, **{k: kwargs.get(k) for k in value_keys})
137
+ return emission | {
138
+ "methodTier": TIER,
139
+ **{k: kwargs.get(k) for k in other_keys}
140
+ }
203
141
 
204
142
 
205
143
  def _get_fuel_category(biomass_category: BiomassCategory, eco_climate_zone: EcoClimateZone) -> FuelCategory:
@@ -210,113 +148,18 @@ def _get_fuel_category(biomass_category: BiomassCategory, eco_climate_zone: EcoC
210
148
  return _BIOMASS_CATEGORY_TO_FUEL_CATEGORY.get(biomass_category, {}).get(eco_climate_zone)
211
149
 
212
150
 
213
- def get_emission_category(fuel_category: FuelCategory) -> EmissionCategory:
214
- """
215
- Get the IPCC (2019) emission category that corresponds to a fuel category.
216
- """
217
- return _FUEL_CATEGORY_TO_EMISSION_CATEGORY.get(fuel_category)
218
-
219
-
220
- def _sample_truncated_normal(
221
- *, iterations: int, value: float, sd: float, seed: Union[int, np.random.Generator, None] = None, **_
222
- ) -> npt.NDArray:
223
- """
224
- Randomly sample a model parameter with a truncated normal distribution. Neither fuel factors nor emission factors
225
- can be below 0, so truncated normal sampling used.
226
- """
227
- return truncated_normal_1d(shape=(1, iterations), mu=value, sigma=sd, low=0, high=np.inf, seed=seed)
228
-
229
-
230
- def _sample_constant(*, iterations: int, value: float, **_) -> npt.NDArray:
231
- """Sample a constant model parameter."""
232
- return repeat_single(shape=(1, iterations), value=value)
233
-
234
-
235
- _KWARGS_TO_SAMPLE_FUNC = {
236
- # ("value", "se", "n"): _sample_standard_error_normal,
237
- ("value", "sd"): _sample_truncated_normal,
238
- ("value",): _sample_constant
239
- }
240
- """
241
- Mapping from available distribution data to sample function.
242
- """
243
-
244
-
245
- def _get_sample_func(kwargs: dict) -> Callable:
246
- """
247
- Select the correct sample function for a parameter based on the distribution data available. All possible
248
- parameters for the model should have, at a minimum, a `value`, meaning that no default function needs to be
249
- specified.
250
-
251
- This function has been extracted into it's own method to allow for mocking of sample function.
252
-
253
- Keyword Args
254
- ------------
255
- value : float
256
- The distribution mean.
257
- sd : float
258
- The standard deviation of the distribution.
259
- se : float
260
- The standard error of the distribution.
261
- n : float
262
- Sample size.
263
-
264
- Returns
265
- -------
266
- Callable
267
- The sample function for the distribution.
268
- """
269
- return next(
270
- sample_func for required_kwargs, sample_func in _KWARGS_TO_SAMPLE_FUNC.items()
271
- if all(kwarg in kwargs.keys() for kwarg in required_kwargs)
272
- )
273
-
274
-
275
- def _get_fuel_factor(fuel_category: FuelCategory) -> dict:
276
- """
277
- Retrieve distribution data for a specific fuel category.
278
- """
279
- LOOKUP_KEY = "ipcc2019FuelCategory_tonnesDryMatterCombustedPerHaBurned"
280
- LOOKUP_FILENAME = f"{LOOKUP_KEY}.csv"
281
- TARGET_DATA = (
282
- "value",
283
- # "se", # useless without n data
284
- # "n" # TODO: add n data to lookup
285
- )
286
-
287
- row = fuel_category.name
288
-
289
- lookup = download_lookup(LOOKUP_FILENAME)
290
-
291
- data = {
292
- target: get_table_value(lookup, "FuelCategory", row, target)
293
- for target in TARGET_DATA
294
- }
295
-
296
- for term_id, target in product(EMISSION_TERM_IDS, TARGET_DATA):
297
- debugMissingLookup(LOOKUP_FILENAME, "FuelCategory", row, target, data.get(target), model=MODEL, term=term_id)
298
-
299
- return (
300
- {
301
- k: parsed for k, v in data.items() if (parsed := safe_parse_float(v, default=None)) is not None
302
- } # remove missing
303
- or _DEFAULT_FACTOR # if parsed dict empty, return default
304
- )
305
-
306
-
307
- def _get_emission_factor(term_id: _EmissionTermId, emission_category: EmissionCategory) -> dict:
151
+ def _get_emission_factor(term_id: str, emission_category: EmissionCategory) -> dict:
308
152
  """
309
153
  Retrieve distribution data for a specific emission and emission category.
310
154
  """
311
155
  TERM_TYPE = "emission"
312
156
  TARGET_DATA = ("value", "sd")
313
-
314
- column_root = f"IPCC_2019_G_EMITTED_PER_KG_DRY_MATTER_COMBUSTED_{emission_category.name}"
157
+ COLUMN_ROOT = "IPCC_2019_G_EMITTED_PER_KG_DRY_MATTER_COMBUSTED"
315
158
 
316
159
  data = {
317
160
  target: get_lookup_value(
318
161
  {"@id": term_id, "termType": TERM_TYPE},
319
- f"{column_root}_{target}",
162
+ "_".join([COLUMN_ROOT, emission_category.name, target]),
320
163
  model=MODEL,
321
164
  term=term_id
322
165
  ) for target in TARGET_DATA
@@ -326,23 +169,12 @@ def _get_emission_factor(term_id: _EmissionTermId, emission_category: EmissionCa
326
169
  {
327
170
  k: parsed for k, v in data.items() if (parsed := safe_parse_float(v, default=None)) is not None
328
171
  } # remove missing
329
- or _DEFAULT_FACTOR # if parsed dict empty, return default
172
+ or DEFAULT_FACTOR # if parsed dict empty, return default
330
173
  )
331
174
 
332
175
 
333
- def _sample_fuel_factor(
334
- fuel_category: FuelCategory, *, seed: Union[int, np.random.Generator, None] = None
335
- ) -> npt.NDArray:
336
- """
337
- Generate random samples from a fuel factor's distribution data.
338
- """
339
- factor_data = _get_fuel_factor(fuel_category)
340
- sample_func = _get_sample_func(factor_data)
341
- return sample_func(iterations=_ITERATIONS, seed=seed, **factor_data)
342
-
343
-
344
- def _sample_emission_factor(
345
- term_id: _EmissionTermId,
176
+ def sample_emission_factor(
177
+ term_id: str,
346
178
  emission_category: EmissionCategory,
347
179
  *,
348
180
  seed: Union[int, np.random.Generator, None] = None
@@ -351,37 +183,8 @@ def _sample_emission_factor(
351
183
  Generate random samples from an emission factor's distribution data.
352
184
  """
353
185
  factor_data = _get_emission_factor(term_id, emission_category)
354
- sample_func = _get_sample_func(factor_data)
355
- return sample_func(iterations=_ITERATIONS, seed=seed, **factor_data)
356
-
357
-
358
- def _emission(term_id: str, **kwargs) -> dict:
359
- """
360
- Build a HESTIA [Emission node](https://www.hestia.earth/schema/Emission) using model output data.
361
- """
362
- emission = _new_emission(term=term_id, model=MODEL)
363
- return emission | {
364
- **{k: v for k, v in kwargs.items()},
365
- "methodTier": TIER
366
- }
367
-
368
-
369
- def _get_site(cycle: dict) -> dict:
370
- """
371
- Get the site data from a [Cycle node](https://www.hestia.earth/schema/Cycle).
372
-
373
- Used as a test utility to mock the 'site' data in during testing.
374
- """
375
- return cycle.get("site", {})
376
-
377
-
378
- def get_percent_burned(site: str):
379
- LOOKUP_KEY = "region-percentageAreaBurnedDuringForestClearance"
380
- LOOKUP_FILENAME = f"{LOOKUP_KEY}.csv"
381
- country_id = site.get("country", {}).get("@id")
382
-
383
- value = get_region_lookup_value(LOOKUP_FILENAME, country_id, LOOKUPS[LOOKUP_KEY])
384
- return safe_parse_float(value, _DEFAULT_PERCENT_BURNED)
186
+ sample_func = get_sample_func(factor_data)
187
+ return sample_func(iterations=ITERATIONS, seed=seed, **factor_data)
385
188
 
386
189
 
387
190
  def _calc_burnt_fuel(area_converted: npt.NDArray, fuel_factor: npt.NDArray, frac_burnt: npt.NDArray) -> npt.NDArray:
@@ -466,38 +269,6 @@ def _build_fuel_burnt_accumulator(
466
269
  return accumulate_fuel_burnt
467
270
 
468
271
 
469
- def _calc_emission(fuel_burnt: npt.NDArray, emission_factor: npt.NDArray,) -> npt.NDArray:
470
- """
471
- Calculate the emission from a fuel burning.
472
-
473
- Parameters
474
- ----------
475
- fuel_burnt : NDArray
476
- The mass of burnt fuel (kg).
477
- emission_factor : NDArray
478
- Conversion factor (kg emission per kg of fuel burnt).
479
-
480
- Returns
481
- -------
482
- NDArray
483
- The mass of emission (kg)
484
- """
485
- return fuel_burnt * emission_factor
486
-
487
-
488
- def _sum_cycle_emissions(term_id: _EmissionTermId, cycle_id: str, inventory: _Inventory) -> npt.NDArray:
489
- """
490
- Sum the emissions allocated to a cycle.
491
- """
492
- KEY = "allocated_emissions"
493
-
494
- def add_cycle_emissions(result: npt.NDArray, year: int) -> npt.NDArray:
495
- allocated_emissions = inventory.get(year, {}).get(KEY, {}).get(term_id, {})
496
- return result + allocated_emissions.get(cycle_id, np.array(0))
497
-
498
- return reduce(add_cycle_emissions, inventory.keys(), np.array(0))
499
-
500
-
501
272
  def _compile_inventory(
502
273
  cycle: dict, site: dict, land_cover_nodes: list[dict], eco_climate_zone: EcoClimateZone
503
274
  ):
@@ -521,7 +292,7 @@ def _compile_inventory(
521
292
  -------
522
293
  should_run : bool
523
294
  Whether the model should be run.
524
- inventory : _Inventory
295
+ inventory : Inventory
525
296
  An inventory of model data.
526
297
  logs : dict
527
298
  Data about the inventory compilation to be logged.
@@ -537,22 +308,22 @@ def _compile_inventory(
537
308
  percent_burned = get_percent_burned(site)
538
309
 
539
310
  @lru_cache(maxsize=len(FuelCategory))
540
- def sample_fuel_factor(*args):
311
+ def sample_fuel_factor_(*args):
541
312
  """Fuel factors should not be re-sampled between years, so cache results."""
542
- return _sample_fuel_factor(*args, seed=rng)
313
+ return sample_fuel_factor(*args, _EMISSION_TERM_IDS, seed=rng)
543
314
 
544
- @lru_cache(maxsize=len(EMISSION_TERM_IDS)*len(EmissionCategory))
545
- def sample_emission_factor(*args):
315
+ @lru_cache(maxsize=len(_EMISSION_TERM_IDS)*len(EmissionCategory))
316
+ def sample_emission_factor_(*args):
546
317
  """Emission factors should not be re-sampled between years, so cache results."""
547
- return _sample_emission_factor(*args, seed=rng)
318
+ return sample_emission_factor(*args, seed=rng)
548
319
 
549
- accumulate_fuel_burnt = _build_fuel_burnt_accumulator(percent_burned, eco_climate_zone, sample_fuel_factor)
320
+ accumulate_fuel_burnt = _build_fuel_burnt_accumulator(percent_burned, eco_climate_zone, sample_fuel_factor_)
550
321
 
551
- def build_inventory_year(inventory: _Inventory, year: int) -> dict:
322
+ def build_inventory_year(inventory: Inventory, year: int) -> dict:
552
323
  """
553
324
  Parameters
554
325
  ----------
555
- inventory : _Inventory
326
+ inventory : Inventory
556
327
  An inventory of model data.
557
328
  year : int
558
329
  The year of the inventory to build.
@@ -575,7 +346,7 @@ def _compile_inventory(
575
346
 
576
347
  natural_vegetation_delta = {
577
348
  category: biomass_category_summary.get(category, 0) - prev_biomass_category_summary.get(category, 0)
578
- for category in _NATURAL_VEGETATION_CATEGORIES
349
+ for category in NATURAL_VEGETATION_CATEGORIES
579
350
  }
580
351
 
581
352
  fuel_burnt_per_category = reduce(
@@ -586,13 +357,13 @@ def _compile_inventory(
586
357
 
587
358
  annual_emissions = {
588
359
  term_id: sum(
589
- _calc_emission(amount, sample_emission_factor(term_id, get_emission_category(fuel_category)))
360
+ calc_emission(amount, sample_emission_factor_(term_id, get_emission_category(fuel_category)))
590
361
  for fuel_category, amount in fuel_burnt_per_category.items()
591
- ) for term_id in EMISSION_TERM_IDS
362
+ ) for term_id in _EMISSION_TERM_IDS
592
363
  }
593
364
 
594
365
  previous_years = list(inventory.keys())
595
- amortisation_slice_index = max(0, len(previous_years) - (_AMORTISATION_PERIOD - 1))
366
+ amortisation_slice_index = max(0, len(previous_years) - (AMORTISATION_PERIOD - 1))
596
367
  amortisation_years = previous_years[amortisation_slice_index:] # get the previous 19 years, if available
597
368
 
598
369
  amortised_emissions = {
@@ -600,7 +371,7 @@ def _compile_inventory(
600
371
  annual_emissions[term_id] + sum(
601
372
  inventory[year_]["annual_emissions"][term_id] for year_ in amortisation_years
602
373
  )
603
- ) for term_id in EMISSION_TERM_IDS
374
+ ) for term_id in _EMISSION_TERM_IDS
604
375
  }
605
376
 
606
377
  cycles = cycles_grouped.get(year, [])
@@ -616,10 +387,10 @@ def _compile_inventory(
616
387
  cycle_id: share_of_emission * amortised_emissions[term_id]
617
388
  for cycle_id, share_of_emission in share_of_emissions.items()
618
389
  }
619
- for term_id in EMISSION_TERM_IDS
390
+ for term_id in _EMISSION_TERM_IDS
620
391
  }
621
392
 
622
- inventory[year] = _InventoryYear(
393
+ inventory[year] = InventoryYear(
623
394
  biomass_category_summary=biomass_category_summary,
624
395
  natural_vegetation_delta=natural_vegetation_delta,
625
396
  fuel_burnt_per_category=fuel_burnt_per_category,
@@ -649,147 +420,6 @@ def _compile_inventory(
649
420
  return should_run, inventory, logs
650
421
 
651
422
 
652
- def _format_column_header(*keys: tuple[Union[Enum, str], ...]) -> str:
653
- """Format a variable number of enums and strings for logging as a table column header."""
654
- return " ".join(format_str(k.value if isinstance(k, Enum) else format_str(k)) for k in keys)
655
-
656
-
657
- def _format_eco_climate_zone(value: EcoClimateZone) -> str:
658
- """Format an eco-climate zone for logging."""
659
- return (
660
- format_str(str(value.name).lower().replace("_", " ").capitalize()) if isinstance(value, EcoClimateZone)
661
- else "None"
662
- )
663
-
664
-
665
- _LOGS_FORMAT_DATA: dict[str, Callable] = {
666
- "has_valid_site_type": format_bool,
667
- "eco_climate_zone": _format_eco_climate_zone,
668
- "has_valid_eco_climate_zone": format_bool,
669
- "has_land_cover_nodes": format_bool,
670
- "should_compile_inventory": format_bool,
671
- "percent_burned": lambda x: format_float(x, "pct"),
672
- }
673
- _DEFAULT_FORMAT_FUNC = format_str
674
-
675
-
676
- def _format_logs(logs: dict) -> dict[str, str]:
677
- """
678
- Format model logs - excluding the inventory data, which must be formatted separately.
679
- """
680
- return {key: _LOGS_FORMAT_DATA.get(key, _DEFAULT_FORMAT_FUNC)(value) for key, value in logs.items()}
681
-
682
-
683
- _INVENTORY_FORMAT_DATA: dict[_InventoryKey, dict[Literal["filter_by", "format_func"], Any]] = {
684
- "fuel_burnt_per_category": {
685
- "format_func": lambda x: format_nd_array(x, "kg")
686
- },
687
- "annual_emissions": {
688
- "filter_by": ("term_id", ),
689
- "format_func": lambda x: format_nd_array(x, "kg")
690
- },
691
- "amortised_emissions": {
692
- "filter_by": ("term_id", ),
693
- "format_func": lambda x: format_nd_array(x, "kg")
694
- },
695
- "share_of_emissions": {
696
- "filter_by": ("cycle_id", ),
697
- "format_func": format_decimal_percentage
698
- },
699
- "allocated_emissions": {
700
- "filter_by": ("term_id", "cycle_id"),
701
- "format_func": lambda x: format_nd_array(x, "kg")
702
- }
703
- }
704
- """
705
- Mapping between inventory key and formatting options for logging in a table. Inventory keys not included in the dict
706
- will not be logged in the table.
707
- """
708
-
709
-
710
- def _flatten_dict(d: dict) -> dict[tuple, Any]:
711
- """
712
- Flatten a nested dict, returns dict with keys as tuples with format `(key_level_1, key_level_2, ..., key_level_n)`.
713
- """
714
- def flatten(d: dict, c: Optional[list] = None):
715
- c = c or []
716
- for a, b in d.items():
717
- if not isinstance(b, dict):
718
- yield (tuple(c+[a]), b)
719
- else:
720
- yield from flatten(b, c+[a])
721
-
722
- return dict(flatten(d))
723
-
724
-
725
- def _get_relevant_inner_keys(
726
- term_id: _EmissionTermId,
727
- cycle_id: str,
728
- key: str,
729
- inventory: _Inventory,
730
- *,
731
- filter_by: Optional[tuple[Literal["term_id", "cycle_id"], ...]] = None,
732
- **_
733
- ) -> list[tuple]:
734
- """
735
- Get the column headings for the formatted table. Nested inventory values should be flattened, with nested keys
736
- being transformed into a tuple with the format `(key_level_1, key_level_2, ..., key_level_n)`.
737
-
738
- Inner keys not relevant to the emission term being logged or the cycle the model is running on should be excluded.
739
- """
740
- FILTER_VALUES = {"term_id": term_id, "cycle_id": cycle_id}
741
- filter_target = (
742
- tuple(val for f in filter_by if (val := FILTER_VALUES.get(f)))
743
- if filter_by else None
744
- )
745
-
746
- inner_keys = {
747
- tuple(k) for inner in inventory.values() for k in _flatten_dict(inner.get(key, {}))
748
- if not filter_target or k == filter_target
749
- }
750
-
751
- return sorted(
752
- inner_keys,
753
- key=lambda category: category.value if isinstance(category, Enum) else str(category)
754
- )
755
-
756
-
757
- def _format_inventory(term_id: _EmissionTermId, cycle_id: str, inventory: dict) -> str:
758
- """
759
- Format the inventory for logging as a table.
760
-
761
- Extract relevant data, flatten nested dicts and format inventory values based on expected data type.
762
- """
763
- relevant_inventory_keys = {
764
- inventory_key: _get_relevant_inner_keys(term_id, cycle_id, inventory_key, inventory, **kwargs)
765
- for inventory_key, kwargs in _INVENTORY_FORMAT_DATA.items()
766
- }
767
-
768
- return log_as_table(
769
- {
770
- "year": year,
771
- **{
772
- _format_column_header(inventory_key, *inner_key): _INVENTORY_FORMAT_DATA[inventory_key]["format_func"](
773
- reduce(lambda d, k: d.get(k, {}), [year, inventory_key, *inner_key], inventory)
774
- )
775
- for inventory_key, relevant_inner_keys in relevant_inventory_keys.items()
776
- for inner_key in relevant_inner_keys
777
- }
778
- } for year in inventory
779
- ) if inventory else "None"
780
-
781
-
782
- def _log_emission_data(should_run: bool, term_id: _EmissionTermId, cycle: dict, inventory: dict, logs: dict):
783
- """
784
- Format and log the model logs and inventory.
785
- """
786
- formatted_logs = _format_logs(logs)
787
- formatted_inventory = _format_inventory(term_id, cycle.get("@id"), inventory)
788
-
789
- logRequirements(cycle, model=MODEL, term=term_id, **formatted_logs, inventory=formatted_inventory)
790
- logShouldRun(cycle, MODEL, term_id, should_run, methodTier=TIER)
791
-
792
-
793
423
  def _should_run(cycle: dict):
794
424
  """
795
425
  Extract, organise and pre-process required data from the input [Cycle node](https://www.hestia.earth/schema/Site)
@@ -802,18 +432,18 @@ def _should_run(cycle: dict):
802
432
 
803
433
  Returns
804
434
  -------
805
- tuple[bool, _Inventory]
435
+ tuple[bool, Inventory]
806
436
  should_run, inventory
807
437
  """
808
- site = _get_site(cycle)
438
+ site = cycle.get("site", {})
809
439
 
810
440
  site_type = site.get("siteType")
811
441
  eco_climate_zone = get_eco_climate_zone_value(site, as_enum=True)
812
442
 
813
443
  land_cover_nodes = get_valid_management_nodes(site)
814
444
 
815
- has_valid_site_type = all([site_type, site_type not in _EXCLUDED_SITE_TYPES])
816
- has_valid_eco_climate_zone = all([eco_climate_zone, eco_climate_zone not in _EXCLUDED_ECO_CLIMATE_ZONES])
445
+ has_valid_site_type = all([site_type, site_type not in EXCLUDED_SITE_TYPES])
446
+ has_valid_eco_climate_zone = all([eco_climate_zone, eco_climate_zone not in EXCLUDED_ECO_CLIMATE_ZONES])
817
447
  has_land_cover_nodes = len(land_cover_nodes) > 1
818
448
 
819
449
  should_compile_inventory = all([
@@ -838,22 +468,12 @@ def _should_run(cycle: dict):
838
468
  **compilation_logs
839
469
  }
840
470
 
841
- for term_id in EMISSION_TERM_IDS:
842
- _log_emission_data(should_run, term_id, cycle, inventory, logs)
471
+ for term_id in _EMISSION_TERM_IDS:
472
+ log_emission_data(should_run, term_id, cycle, inventory, logs)
843
473
 
844
474
  return should_run, inventory
845
475
 
846
476
 
847
- def _run_emission(term_id: _EmissionTermId, cycle_id: str, inventory: _Inventory) -> list[dict]:
848
- """
849
- Retrieve the sum relevant emissions and format them as a HESTIA
850
- [Emission node](https://www.hestia.earth/schema/Emission).
851
- """
852
- emission = _sum_cycle_emissions(term_id, cycle_id, inventory)
853
- descriptive_stats = calc_descriptive_stats(emission, STATS_DEFINITION, decimals=3)
854
- return _emission(term_id, **descriptive_stats)
855
-
856
-
857
477
  def run(cycle: dict):
858
478
  """
859
479
  Run the `nonCo2EmissionsToAirNaturalVegetationBurning` model on a Cycle.
@@ -871,4 +491,7 @@ def run(cycle: dict):
871
491
  `n2OToAirNaturalVegetationBurningDirect` **OR** `noxToAirNaturalVegetationBurning`.
872
492
  """
873
493
  should_run, inventory = _should_run(cycle)
874
- return [_run_emission(term_id, cycle.get("@id"), inventory) for term_id in EMISSION_TERM_IDS] if should_run else []
494
+ return (
495
+ [_emission(*run_emission(term_id, cycle.get("@id"), inventory)) for term_id in _EMISSION_TERM_IDS]
496
+ if should_run else []
497
+ )
@@ -129,13 +129,13 @@ def calc_emission(
129
129
  "n2OToAirOrganicSoilCultivationDirect"
130
130
  ],
131
131
  emission_factor: float,
132
- histosol: float,
132
+ organic_soils: float,
133
133
  land_occupation: float
134
134
  ):
135
135
  """
136
136
  Calculate the emission and convert it to kg/ha-1.
137
137
  """
138
- return emission_factor * land_occupation * histosol * _CONVERSION_FACTORS.get(emission_id, 1) / 100
138
+ return emission_factor * land_occupation * organic_soils * _CONVERSION_FACTORS.get(emission_id, 1) / 100
139
139
 
140
140
 
141
141
  def remap_categories(