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.
- hestia_earth/models/config/Cycle.json +15 -0
- hestia_earth/models/cycle/product/economicValueShare.py +4 -4
- hestia_earth/models/geospatialDatabase/histosol.py +31 -11
- hestia_earth/models/hestia/aboveGroundCropResidueTotal.py +2 -2
- hestia_earth/models/hestia/soilClassification.py +31 -13
- hestia_earth/models/ipcc2019/animal/pastureGrass.py +3 -1
- hestia_earth/models/ipcc2019/burning_utils.py +406 -4
- hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +26 -8
- hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +8 -11
- hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +9 -12
- hestia_earth/models/ipcc2019/emissionsToAirOrganicSoilBurning.py +516 -0
- hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +10 -13
- hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +56 -433
- hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +2 -2
- hestia_earth/models/ipcc2019/pastureGrass.py +3 -1
- hestia_earth/models/mocking/search-results.json +1 -1
- hestia_earth/models/utils/blank_node.py +68 -0
- hestia_earth/models/utils/impact_assessment.py +3 -0
- hestia_earth/models/version.py +1 -1
- hestia_earth/orchestrator/strategies/merge/merge_node.py +32 -2
- {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/METADATA +1 -1
- {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/RECORD +25 -24
- {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.dist-info}/licenses/LICENSE +0 -0
- {hestia_earth_models-0.75.1.dist-info → hestia_earth_models-0.75.2.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
|
|
7
|
-
|
|
8
|
-
from hestia_earth.utils.
|
|
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.
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
172
|
+
or DEFAULT_FACTOR # if parsed dict empty, return default
|
|
330
173
|
)
|
|
331
174
|
|
|
332
175
|
|
|
333
|
-
def
|
|
334
|
-
|
|
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 =
|
|
355
|
-
return sample_func(iterations=
|
|
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 :
|
|
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
|
|
311
|
+
def sample_fuel_factor_(*args):
|
|
541
312
|
"""Fuel factors should not be re-sampled between years, so cache results."""
|
|
542
|
-
return
|
|
313
|
+
return sample_fuel_factor(*args, _EMISSION_TERM_IDS, seed=rng)
|
|
543
314
|
|
|
544
|
-
@lru_cache(maxsize=len(
|
|
545
|
-
def
|
|
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
|
|
318
|
+
return sample_emission_factor(*args, seed=rng)
|
|
548
319
|
|
|
549
|
-
accumulate_fuel_burnt = _build_fuel_burnt_accumulator(percent_burned, eco_climate_zone,
|
|
320
|
+
accumulate_fuel_burnt = _build_fuel_burnt_accumulator(percent_burned, eco_climate_zone, sample_fuel_factor_)
|
|
550
321
|
|
|
551
|
-
def build_inventory_year(inventory:
|
|
322
|
+
def build_inventory_year(inventory: Inventory, year: int) -> dict:
|
|
552
323
|
"""
|
|
553
324
|
Parameters
|
|
554
325
|
----------
|
|
555
|
-
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
|
|
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
|
-
|
|
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
|
|
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) - (
|
|
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
|
|
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
|
|
390
|
+
for term_id in _EMISSION_TERM_IDS
|
|
620
391
|
}
|
|
621
392
|
|
|
622
|
-
inventory[year] =
|
|
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,
|
|
435
|
+
tuple[bool, Inventory]
|
|
806
436
|
should_run, inventory
|
|
807
437
|
"""
|
|
808
|
-
site =
|
|
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
|
|
816
|
-
has_valid_eco_climate_zone = all([eco_climate_zone, eco_climate_zone not in
|
|
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
|
|
842
|
-
|
|
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
|
|
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
|
-
|
|
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 *
|
|
138
|
+
return emission_factor * land_occupation * organic_soils * _CONVERSION_FACTORS.get(emission_id, 1) / 100
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def remap_categories(
|
|
@@ -175,6 +175,7 @@ def _run_practice(cycle: dict, meanDE: float, meanECHHV: float, REM: float, REG:
|
|
|
175
175
|
GE = (
|
|
176
176
|
calculate_GE(animal_values, REM, REG, NEwool, NEm_feed, NEg_feed) / (meanDE/100)
|
|
177
177
|
) if meanDE else 0
|
|
178
|
+
has_positive_GE_value = GE >= 0
|
|
178
179
|
|
|
179
180
|
def run(practice: dict):
|
|
180
181
|
key = practice.get('key', {})
|
|
@@ -206,11 +207,12 @@ def _run_practice(cycle: dict, meanDE: float, meanECHHV: float, REM: float, REG:
|
|
|
206
207
|
logRequirements(cycle, model=MODEL, term=input_term_id, model_key=MODEL_KEY,
|
|
207
208
|
feed_logs=log_as_table(log_feed),
|
|
208
209
|
has_positive_feed_values=has_positive_feed_values,
|
|
210
|
+
has_positive_GE_value=has_positive_GE_value,
|
|
209
211
|
animal_logs=logs,
|
|
210
212
|
animal_lookups=animal_lookups,
|
|
211
213
|
animal_properties=animal_properties)
|
|
212
214
|
|
|
213
|
-
should_run = all([has_positive_feed_values])
|
|
215
|
+
should_run = all([has_positive_feed_values, has_positive_GE_value])
|
|
214
216
|
logShouldRun(cycle, MODEL, input_term_id, should_run, model_key=MODEL_KEY)
|
|
215
217
|
|
|
216
218
|
return _input(input_term_id, value) if should_run else None
|