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
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
from functools import lru_cache, reduce
|
|
2
|
+
import numpy as np
|
|
3
|
+
import numpy.typing as npt
|
|
4
|
+
from typing import Callable, Union
|
|
5
|
+
|
|
6
|
+
from hestia_earth.utils.stats import gen_seed
|
|
7
|
+
from hestia_earth.utils.tools import safe_parse_float
|
|
8
|
+
|
|
9
|
+
from hestia_earth.models.utils import split_on_condition
|
|
10
|
+
from hestia_earth.models.utils.blank_node import (
|
|
11
|
+
closest_end_date, closest_depth, filter_list_term_id, get_node_value, group_nodes_by_year, pick_shallowest,
|
|
12
|
+
select_nodes_by, split_nodes_by_dates
|
|
13
|
+
)
|
|
14
|
+
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
15
|
+
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
16
|
+
from hestia_earth.models.utils.emission import _new_emission
|
|
17
|
+
from hestia_earth.models.utils.site import related_cycles
|
|
18
|
+
from hestia_earth.models.utils.term import get_lookup_value
|
|
19
|
+
|
|
20
|
+
from . import MODEL
|
|
21
|
+
from .biomass_utils import get_valid_management_nodes, summarise_land_cover_nodes
|
|
22
|
+
from .burning_utils import (
|
|
23
|
+
calc_emission, DEFAULT_FACTOR, EmissionCategory, EXCLUDED_ECO_CLIMATE_ZONES, EXCLUDED_SITE_TYPES, FuelCategory,
|
|
24
|
+
ITERATIONS, get_emission_category, get_percent_burned, get_sample_func, Inventory, InventoryYear,
|
|
25
|
+
NATURAL_VEGETATION_CATEGORIES, run_emission, sample_fuel_factor, AMORTISATION_PERIOD, log_emission_data, TIER
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
REQUIREMENTS = {
|
|
29
|
+
"Cycle": {
|
|
30
|
+
"site": {
|
|
31
|
+
"@type": "Site",
|
|
32
|
+
"management": [
|
|
33
|
+
{
|
|
34
|
+
"@type": "Management",
|
|
35
|
+
"value": "",
|
|
36
|
+
"term.termType": "landCover",
|
|
37
|
+
"endDate": "",
|
|
38
|
+
"optional": {
|
|
39
|
+
"startDate": ""
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"measurements": [
|
|
44
|
+
{
|
|
45
|
+
"@type": "Measurement",
|
|
46
|
+
"value": "",
|
|
47
|
+
"term.@id": "ecoClimateZone",
|
|
48
|
+
"none": {
|
|
49
|
+
"value": ["5, 6"]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"@type": "Measurement",
|
|
54
|
+
"value": "",
|
|
55
|
+
"term.@id": "organicSoils"
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"none": {
|
|
59
|
+
"siteType": ["glass or high accessible cover"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
LOOKUPS = {
|
|
65
|
+
"emission": [
|
|
66
|
+
"IPCC_2013_G_EMITTED_PER_KG_DRY_MATTER_COMBUSTED_EXTRATROPICAL_ORGANIC_SOILS_value",
|
|
67
|
+
"IPCC_2013_G_EMITTED_PER_KG_DRY_MATTER_COMBUSTED_TROPICAL_ORGANIC_SOILS_value"
|
|
68
|
+
],
|
|
69
|
+
"ipcc2019FuelCategory_tonnesDryMatterCombustedPerHaBurned": "value",
|
|
70
|
+
"landCover": "BIOMASS_CATEGORY",
|
|
71
|
+
"region-percentageAreaBurnedDuringForestClearance": "percentage_area_burned_during_forest_clearance"
|
|
72
|
+
}
|
|
73
|
+
RETURNS = {
|
|
74
|
+
"Emission": [{
|
|
75
|
+
"value": "",
|
|
76
|
+
"sd": "",
|
|
77
|
+
"min": "",
|
|
78
|
+
"max": "",
|
|
79
|
+
"statsDefinition": "simulated",
|
|
80
|
+
"observations": "",
|
|
81
|
+
"dates": "",
|
|
82
|
+
"methodClassification": "tier 1 model",
|
|
83
|
+
"depth": 30
|
|
84
|
+
}]
|
|
85
|
+
}
|
|
86
|
+
TERM_ID = 'ch4ToAirOrganicSoilBurning,co2ToAirOrganicSoilBurning,coToAirOrganicSoilBurning,n2OToAirOrganicSoilBurningDirect,nh3ToAirOrganicSoilBurning,noxToAirOrganicSoilBurning' # noqa: E501
|
|
87
|
+
|
|
88
|
+
_EMISSION_TERM_IDS = TERM_ID.split(",")
|
|
89
|
+
|
|
90
|
+
_ORGANIC_SOILS_TERM_ID = "organicSoils"
|
|
91
|
+
_DEFAULT_ORGANIC_SOILS = 0
|
|
92
|
+
_DEPTH_UPPER = 0
|
|
93
|
+
_DEPTH_LOWER = 30 # TODO: add depth
|
|
94
|
+
|
|
95
|
+
_CONVERSION_FACTORS = {
|
|
96
|
+
"co2ToAirOrganicSoilBurning": get_atomic_conversion(Units.KG_CO2, Units.TO_C)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_ECO_CLIMATE_ZONE_TO_FUEL_CATEGORY = {
|
|
100
|
+
EcoClimateZone.WARM_TEMPERATE_MOIST: FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
101
|
+
EcoClimateZone.WARM_TEMPERATE_DRY: FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
102
|
+
EcoClimateZone.COOL_TEMPERATE_MOIST: FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
103
|
+
EcoClimateZone.COOL_TEMPERATE_DRY: FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
104
|
+
EcoClimateZone.BOREAL_MOIST: FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
105
|
+
EcoClimateZone.BOREAL_DRY: FuelCategory.DRAINED_EXTRATROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
106
|
+
EcoClimateZone.TROPICAL_MONTANE: FuelCategory.DRAINED_TROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
107
|
+
EcoClimateZone.TROPICAL_WET: FuelCategory.DRAINED_TROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
108
|
+
EcoClimateZone.TROPICAL_MOIST: FuelCategory.DRAINED_TROPICAL_ORGANIC_SOILS_WILDFIRE,
|
|
109
|
+
EcoClimateZone.TROPICAL_DRY: FuelCategory.DRAINED_TROPICAL_ORGANIC_SOILS_WILDFIRE
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _emission(term_id: str, kwargs) -> dict:
|
|
114
|
+
"""
|
|
115
|
+
Build a HESTIA [Emission node](https://www.hestia.earth/schema/Emission) using model output data.
|
|
116
|
+
"""
|
|
117
|
+
value_keys, other_keys = split_on_condition([*kwargs], lambda k: k in ("value", "sd", "min", "max"))
|
|
118
|
+
emission = _new_emission(term=term_id, model=MODEL, **{k: kwargs.get(k) for k in value_keys})
|
|
119
|
+
return emission | {
|
|
120
|
+
"methodTier": TIER,
|
|
121
|
+
"depth": _DEPTH_LOWER,
|
|
122
|
+
**{k: kwargs.get(k) for k in other_keys}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _get_emission_factor(term_id: str, emission_category: EmissionCategory) -> dict:
|
|
127
|
+
"""
|
|
128
|
+
Retrieve distribution data for a specific emission and emission category.
|
|
129
|
+
"""
|
|
130
|
+
TERM_TYPE = "emission"
|
|
131
|
+
TARGET_DATA = "value" # No uncertainty data available yet
|
|
132
|
+
|
|
133
|
+
COLUMN_ROOT = "IPCC_2013_G_EMITTED_PER_KG_DRY_MATTER_COMBUSTED"
|
|
134
|
+
|
|
135
|
+
data = {
|
|
136
|
+
TARGET_DATA: get_lookup_value(
|
|
137
|
+
{"@id": term_id, "termType": TERM_TYPE},
|
|
138
|
+
"_".join([COLUMN_ROOT, emission_category.name, TARGET_DATA]),
|
|
139
|
+
model=MODEL,
|
|
140
|
+
term=term_id
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
{
|
|
146
|
+
k: parsed for k, v in data.items() if (parsed := safe_parse_float(v, default=None)) is not None
|
|
147
|
+
} # remove missing
|
|
148
|
+
or DEFAULT_FACTOR # if parsed dict empty, return default
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _sample_emission_factor(
|
|
153
|
+
term_id: str,
|
|
154
|
+
emission_category: EmissionCategory,
|
|
155
|
+
*,
|
|
156
|
+
seed: Union[int, np.random.Generator, None] = None
|
|
157
|
+
) -> npt.NDArray:
|
|
158
|
+
"""
|
|
159
|
+
Generate random samples from an emission factor's distribution data.
|
|
160
|
+
"""
|
|
161
|
+
factor_data = _get_emission_factor(term_id, emission_category)
|
|
162
|
+
sample_func = get_sample_func(factor_data)
|
|
163
|
+
return sample_func(iterations=ITERATIONS, seed=seed, **factor_data)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _calc_burnt_fuel(
|
|
167
|
+
area_converted: npt.NDArray, fuel_factor: npt.NDArray, frac_burnt: npt.NDArray, frac_organic_soils: float
|
|
168
|
+
) -> npt.NDArray:
|
|
169
|
+
"""
|
|
170
|
+
Calculate the amount of fuel burnt during a fire event.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
area_converted : NDArray
|
|
175
|
+
Area of land converted (ha).
|
|
176
|
+
fuel_factor : NDArray
|
|
177
|
+
Conversion factor (kg fuel per ha of land cover converted).
|
|
178
|
+
frac_burnt : NDArray
|
|
179
|
+
The fraction of land converted using burning during a land use change event (decimal percentage, 0-1).
|
|
180
|
+
frac_organic_soils : float
|
|
181
|
+
The fraction of land occupied by organic soils (decimal percentage, 0-1)
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
NDArray
|
|
186
|
+
The mass of burnt fuel (kg)
|
|
187
|
+
"""
|
|
188
|
+
return area_converted * fuel_factor * frac_burnt * frac_organic_soils
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _build_fuel_burnt_accumulator(
|
|
192
|
+
percent_burned: npt.ArrayLike,
|
|
193
|
+
eco_climate_zone: EcoClimateZone,
|
|
194
|
+
sample_fuel_factor_func: Callable[[FuelCategory], npt.NDArray]
|
|
195
|
+
):
|
|
196
|
+
"""
|
|
197
|
+
Build an `accumulate_fuel_burnt` function to reduce natural vegetation deltas into mass of fuel burnt per
|
|
198
|
+
`_FuelCategory`.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
percent_burned : NDArray
|
|
203
|
+
The percentage of land converted using burning during a land use change event (percentage, 0-100%).
|
|
204
|
+
eco_climate_zone : EcoClimateZone
|
|
205
|
+
The eco-climate zone of the Site.
|
|
206
|
+
sample_fuel_factor_func : Callable[[_FuelCategory], npt.NDArray]
|
|
207
|
+
Function to sample fuel factor parameter.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
NDArray
|
|
212
|
+
The mass of burnt fuel (kg)
|
|
213
|
+
"""
|
|
214
|
+
frac_burnt = percent_burned / 100
|
|
215
|
+
|
|
216
|
+
def accumulate_fuel_burnt(
|
|
217
|
+
result: dict[FuelCategory, npt.NDArray], delta: float, percent_organic_soils: float
|
|
218
|
+
) -> dict[FuelCategory, npt.NDArray]:
|
|
219
|
+
"""
|
|
220
|
+
Calculate the amount of fuel burnt when natural vegetation is lost. Accumulate fuel burnt by `_FuelCategory`.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
result : dict[_FuelCategory, npt.NDArray]
|
|
225
|
+
A dict with the format `{_FuelCategory: kg_fuel_burnt (npt.NDArray)}`.
|
|
226
|
+
delta : float
|
|
227
|
+
The change in land cover for the biomass category (% area).
|
|
228
|
+
percent_organic_soils : NDArray
|
|
229
|
+
The percentage of land occupied by organic soils (percentage, 0-100%).
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
dict[_FuelCategory, npt.NDArray]
|
|
234
|
+
"""
|
|
235
|
+
frac_organic_soils = percent_organic_soils / 100
|
|
236
|
+
|
|
237
|
+
fuel_category = _ECO_CLIMATE_ZONE_TO_FUEL_CATEGORY.get(eco_climate_zone)
|
|
238
|
+
fuel_factor = sample_fuel_factor_func(fuel_category)
|
|
239
|
+
|
|
240
|
+
area_converted = abs(delta) / 100 if delta < 0 else 0 # We only care about losses
|
|
241
|
+
|
|
242
|
+
already_burnt = result.get(fuel_category, np.array(0))
|
|
243
|
+
|
|
244
|
+
update_dict = {} if area_converted == 0 else {
|
|
245
|
+
fuel_category: already_burnt + _calc_burnt_fuel(area_converted, fuel_factor, frac_burnt, frac_organic_soils)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result | update_dict
|
|
249
|
+
|
|
250
|
+
return accumulate_fuel_burnt
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _compileInventory(
|
|
254
|
+
cycle: dict,
|
|
255
|
+
site: dict,
|
|
256
|
+
land_cover_nodes: list[dict],
|
|
257
|
+
organic_soil_nodes: list[dict],
|
|
258
|
+
eco_climate_zone: EcoClimateZone
|
|
259
|
+
):
|
|
260
|
+
"""
|
|
261
|
+
Compile the run data for the model, collating data from `site.management` and related cycles. An annualised
|
|
262
|
+
inventory of land cover change and natural vegetation burning events is constructed. Emissions from burning events
|
|
263
|
+
are estimated, amortised over 20 years and allocated to cycles.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
cycle : dict
|
|
268
|
+
The HESTIA [Cycle](https://www.hestia.earth/schema/Cycle) the model is running on.
|
|
269
|
+
site : dict
|
|
270
|
+
The HESTIA [Site](https://www.hestia.earth/schema/Site) the Cycle takes place on.
|
|
271
|
+
land_cover_nodes : list[dict]
|
|
272
|
+
Valid land cover [Management nodes](https://www.hestia.earth/schema/Management) extracted from the Site.
|
|
273
|
+
organic_soil_nodes : list[dict]
|
|
274
|
+
Valid `organicSoils` [Measurement nodes](https://www.hestia.earth/schema/Measurement) extracted from the Site.
|
|
275
|
+
eco_climate_zone : EcoClimateZone
|
|
276
|
+
The eco-climate zone of the Site.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
should_run : bool
|
|
281
|
+
Whether the model should be run.
|
|
282
|
+
inventory : Inventory
|
|
283
|
+
An inventory of model data.
|
|
284
|
+
logs : dict
|
|
285
|
+
Data about the inventory compilation to be logged.
|
|
286
|
+
"""
|
|
287
|
+
cycle_id = cycle.get("@id")
|
|
288
|
+
related_cycles_ = related_cycles(site, cycles_mapping={cycle_id: cycle})
|
|
289
|
+
|
|
290
|
+
seed = gen_seed(site, MODEL, TERM_ID)
|
|
291
|
+
rng = np.random.default_rng(seed)
|
|
292
|
+
|
|
293
|
+
cycles_grouped = group_nodes_by_year(related_cycles_)
|
|
294
|
+
land_cover_grouped = group_nodes_by_year(land_cover_nodes)
|
|
295
|
+
percent_burned = get_percent_burned(site)
|
|
296
|
+
|
|
297
|
+
@lru_cache(maxsize=len(FuelCategory))
|
|
298
|
+
def sample_fuel_factor_(*args):
|
|
299
|
+
"""Fuel factors should not be re-sampled between years, so cache results."""
|
|
300
|
+
return sample_fuel_factor(*args, _EMISSION_TERM_IDS, seed=rng)
|
|
301
|
+
|
|
302
|
+
@lru_cache(maxsize=len(_EMISSION_TERM_IDS)*len(EmissionCategory))
|
|
303
|
+
def sample_emission_factor_(*args):
|
|
304
|
+
"""Emission factors should not be re-sampled between years, so cache results."""
|
|
305
|
+
return _sample_emission_factor(*args, seed=rng)
|
|
306
|
+
|
|
307
|
+
accumulate_fuel_burnt = _build_fuel_burnt_accumulator(
|
|
308
|
+
percent_burned,
|
|
309
|
+
eco_climate_zone,
|
|
310
|
+
sample_fuel_factor_
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def buildInventory_year(inventory: Inventory, year: int) -> dict:
|
|
314
|
+
"""
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
inventory : Inventory
|
|
318
|
+
An inventory of model data.
|
|
319
|
+
year : int
|
|
320
|
+
The year of the inventory to build.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
inventory : dict
|
|
325
|
+
An inventory of model data, updated to include the input year.
|
|
326
|
+
"""
|
|
327
|
+
most_relevant_organic_soils_node = select_nodes_by(
|
|
328
|
+
organic_soil_nodes,
|
|
329
|
+
[
|
|
330
|
+
lambda nodes: closest_end_date(nodes, year),
|
|
331
|
+
lambda nodes: pick_shallowest(nodes, default={}) # resolve down to a single node
|
|
332
|
+
]
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
percent_organic_soils = get_node_value(most_relevant_organic_soils_node, default=_DEFAULT_ORGANIC_SOILS)
|
|
336
|
+
|
|
337
|
+
land_cover_nodes = land_cover_grouped.get(
|
|
338
|
+
next(
|
|
339
|
+
(k for k in sorted(land_cover_grouped) if k >= year), # backfill if possible
|
|
340
|
+
min(land_cover_grouped, key=lambda k: abs(k - year)) # else forward-fill
|
|
341
|
+
),
|
|
342
|
+
[]
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
biomass_category_summary = summarise_land_cover_nodes(land_cover_nodes)
|
|
346
|
+
prev_biomass_category_summary = inventory.get(year-1, {}).get("biomass_category_summary", {})
|
|
347
|
+
|
|
348
|
+
natural_vegetation_delta = {
|
|
349
|
+
category: biomass_category_summary.get(category, 0) - prev_biomass_category_summary.get(category, 0)
|
|
350
|
+
for category in NATURAL_VEGETATION_CATEGORIES
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fuel_burnt_per_category = reduce(
|
|
354
|
+
lambda result, delta: accumulate_fuel_burnt(result, delta, percent_organic_soils),
|
|
355
|
+
natural_vegetation_delta.values(),
|
|
356
|
+
dict()
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
annual_emissions = {
|
|
360
|
+
term_id: sum(
|
|
361
|
+
calc_emission(
|
|
362
|
+
amount,
|
|
363
|
+
sample_emission_factor_(term_id, get_emission_category(fuel_category)),
|
|
364
|
+
_CONVERSION_FACTORS.get(term_id, 1)
|
|
365
|
+
)
|
|
366
|
+
for fuel_category, amount in fuel_burnt_per_category.items()
|
|
367
|
+
) for term_id in _EMISSION_TERM_IDS
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
previous_years = list(inventory.keys())
|
|
371
|
+
amortisation_slice_index = max(0, len(previous_years) - (AMORTISATION_PERIOD - 1))
|
|
372
|
+
amortisation_years = previous_years[amortisation_slice_index:] # get the previous 19 years, if available
|
|
373
|
+
|
|
374
|
+
amortised_emissions = {
|
|
375
|
+
term_id: 0.05 * (
|
|
376
|
+
annual_emissions[term_id] + sum(
|
|
377
|
+
inventory[year_]["annual_emissions"][term_id] for year_ in amortisation_years
|
|
378
|
+
)
|
|
379
|
+
) for term_id in _EMISSION_TERM_IDS
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
cycles = cycles_grouped.get(year, [])
|
|
383
|
+
total_cycle_duration = sum(c.get("fraction_of_group_duration", 0) for c in cycles)
|
|
384
|
+
|
|
385
|
+
share_of_emissions = {
|
|
386
|
+
cycle["@id"]: cycle.get("fraction_of_group_duration", 0) / total_cycle_duration
|
|
387
|
+
for cycle in cycles
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
allocated_emissions = {
|
|
391
|
+
term_id: {
|
|
392
|
+
cycle_id: share_of_emission * amortised_emissions[term_id]
|
|
393
|
+
for cycle_id, share_of_emission in share_of_emissions.items()
|
|
394
|
+
}
|
|
395
|
+
for term_id in _EMISSION_TERM_IDS
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
inventory[year] = InventoryYear(
|
|
399
|
+
biomass_category_summary=biomass_category_summary,
|
|
400
|
+
natural_vegetation_delta=natural_vegetation_delta,
|
|
401
|
+
fuel_burnt_per_category=fuel_burnt_per_category,
|
|
402
|
+
annual_emissions=annual_emissions,
|
|
403
|
+
amortised_emissions=amortised_emissions,
|
|
404
|
+
share_of_emissions=share_of_emissions,
|
|
405
|
+
allocated_emissions=allocated_emissions,
|
|
406
|
+
percent_organic_soils=percent_organic_soils
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return inventory
|
|
410
|
+
|
|
411
|
+
all_years = list(cycles_grouped.keys()) + list(land_cover_grouped.keys())
|
|
412
|
+
min_year, max_year = min(all_years), max(all_years)
|
|
413
|
+
|
|
414
|
+
inventory = reduce(buildInventory_year, range(min_year, max_year+1), dict())
|
|
415
|
+
|
|
416
|
+
n_land_cover_years = len(land_cover_grouped)
|
|
417
|
+
|
|
418
|
+
logs = {
|
|
419
|
+
"n_land_cover_years": n_land_cover_years,
|
|
420
|
+
"percent_burned": percent_burned,
|
|
421
|
+
"seed": seed,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
should_run = bool(inventory and n_land_cover_years > 1)
|
|
425
|
+
|
|
426
|
+
return should_run, inventory, logs
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _should_run(cycle: dict):
|
|
430
|
+
"""
|
|
431
|
+
Extract, organise and pre-process required data from the input [Cycle node](https://www.hestia.earth/schema/Site)
|
|
432
|
+
and determine whether the model should run.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
cycle : dict
|
|
437
|
+
A HESTIA [Cycle](https://www.hestia.earth/schema/Cycle).
|
|
438
|
+
|
|
439
|
+
Returns
|
|
440
|
+
-------
|
|
441
|
+
tuple[bool, Inventory]
|
|
442
|
+
should_run, inventory
|
|
443
|
+
"""
|
|
444
|
+
site = cycle.get("site", {})
|
|
445
|
+
|
|
446
|
+
site_type = site.get("siteType")
|
|
447
|
+
eco_climate_zone = get_eco_climate_zone_value(site, as_enum=True)
|
|
448
|
+
|
|
449
|
+
land_cover_nodes = get_valid_management_nodes(site)
|
|
450
|
+
|
|
451
|
+
organic_soil_nodes = split_nodes_by_dates(
|
|
452
|
+
select_nodes_by(
|
|
453
|
+
site.get("measurements", []),
|
|
454
|
+
[
|
|
455
|
+
lambda nodes: filter_list_term_id(nodes, _ORGANIC_SOILS_TERM_ID),
|
|
456
|
+
lambda nodes: closest_depth(nodes, _DEPTH_UPPER, _DEPTH_LOWER, depth_strict=False)
|
|
457
|
+
]
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
has_valid_site_type = all([site_type, site_type not in EXCLUDED_SITE_TYPES])
|
|
462
|
+
has_valid_eco_climate_zone = all([eco_climate_zone, eco_climate_zone not in EXCLUDED_ECO_CLIMATE_ZONES])
|
|
463
|
+
has_land_cover_nodes = len(land_cover_nodes) > 1
|
|
464
|
+
has_organic_soil_nodes = bool(organic_soil_nodes)
|
|
465
|
+
|
|
466
|
+
should_compileInventory = all([
|
|
467
|
+
has_valid_site_type,
|
|
468
|
+
has_valid_eco_climate_zone,
|
|
469
|
+
has_land_cover_nodes,
|
|
470
|
+
has_organic_soil_nodes
|
|
471
|
+
])
|
|
472
|
+
|
|
473
|
+
should_run, inventory, compilation_logs = (
|
|
474
|
+
_compileInventory(cycle, site, land_cover_nodes, organic_soil_nodes, eco_climate_zone)
|
|
475
|
+
if should_compileInventory else (False, {}, {})
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
logs = {
|
|
479
|
+
"site_id": site.get("@id"),
|
|
480
|
+
"site_type": site_type,
|
|
481
|
+
"has_valid_site_type": has_valid_site_type,
|
|
482
|
+
"eco_climate_zone": eco_climate_zone,
|
|
483
|
+
"has_valid_eco_climate_zone": has_valid_eco_climate_zone,
|
|
484
|
+
"has_land_cover_nodes": has_land_cover_nodes,
|
|
485
|
+
"has_organic_soil_nodes": has_organic_soil_nodes,
|
|
486
|
+
"should_compileInventory": should_compileInventory,
|
|
487
|
+
**compilation_logs
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for term_id in _EMISSION_TERM_IDS:
|
|
491
|
+
log_emission_data(should_run, term_id, cycle, inventory, logs)
|
|
492
|
+
|
|
493
|
+
return should_run, inventory
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def run(cycle: dict):
|
|
497
|
+
"""
|
|
498
|
+
Run the `nonCo2EmissionsToAirOrganicSoilBurning` model on a Cycle.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
cycle : dict
|
|
503
|
+
A HESTIA [Cycle](https://www.hestia.earth/schema/Cycle).
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
list[dict]
|
|
508
|
+
A list of HESTIA [Emission](https://www.hestia.earth/schema/Emission) nodes with `term.termType` =
|
|
509
|
+
`ch4ToAirOrganicSoilBurning` **OR** `co2ToAirOrganicSoilBurning` **OR** `coToAirOrganicSoilBurning` **OR**
|
|
510
|
+
`n2OToAirOrganicSoilBurningDirect` **OR** `noxToAirOrganicSoilBurning`.
|
|
511
|
+
"""
|
|
512
|
+
should_run, inventory = _should_run(cycle)
|
|
513
|
+
return (
|
|
514
|
+
[_emission(*run_emission(term_id, cycle.get("@id"), inventory)) for term_id in _EMISSION_TERM_IDS]
|
|
515
|
+
if should_run else []
|
|
516
|
+
)
|
|
@@ -36,7 +36,7 @@ REQUIREMENTS = {
|
|
|
36
36
|
"site": {
|
|
37
37
|
"@type": "Site",
|
|
38
38
|
"measurements": [
|
|
39
|
-
{"@type": "Measurement", "value": "", "term.@id": "
|
|
39
|
+
{"@type": "Measurement", "value": "", "term.@id": "organicSoils"},
|
|
40
40
|
{"@type": "Measurement", "value": "", "term.@id": "ecoClimateZone"}
|
|
41
41
|
]
|
|
42
42
|
},
|
|
@@ -103,10 +103,7 @@ def _should_run(cycle: dict):
|
|
|
103
103
|
site = cycle.get('site', {})
|
|
104
104
|
measurements = site.get('measurements', [])
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
return most_relevant_measurement_value(measurements, term_id, end_date)
|
|
108
|
-
|
|
109
|
-
histosol = _get_measurement_content('histosol')
|
|
106
|
+
organic_soils = most_relevant_measurement_value(measurements, "organicSoils", end_date)
|
|
110
107
|
eco_climate_zone = get_eco_climate_zone_value(cycle, as_enum=True)
|
|
111
108
|
organic_soil_category = assign_organic_soil_category(cycle, log_id=TERM_ID)
|
|
112
109
|
|
|
@@ -124,7 +121,7 @@ def _should_run(cycle: dict):
|
|
|
124
121
|
organic_soil_category=organic_soil_category,
|
|
125
122
|
emission_factor=f"{format_float(emission_factor_mean)} ± {format_float(emission_factor_sd)}",
|
|
126
123
|
land_occupation=format_float(land_occupation),
|
|
127
|
-
|
|
124
|
+
organic_soils=format_float(organic_soils)
|
|
128
125
|
)
|
|
129
126
|
|
|
130
127
|
should_run = all([
|
|
@@ -135,22 +132,22 @@ def _should_run(cycle: dict):
|
|
|
135
132
|
emission_factor_mean,
|
|
136
133
|
emission_factor_sd,
|
|
137
134
|
land_occupation,
|
|
138
|
-
|
|
135
|
+
organic_soils
|
|
139
136
|
]
|
|
140
137
|
)
|
|
141
138
|
])
|
|
142
139
|
|
|
143
140
|
logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
|
|
144
141
|
|
|
145
|
-
return should_run, emission_factor_mean, emission_factor_sd,
|
|
142
|
+
return should_run, emission_factor_mean, emission_factor_sd, organic_soils, land_occupation
|
|
146
143
|
|
|
147
144
|
|
|
148
|
-
def _run(emission_factor_mean: float, emission_factor_sd: float,
|
|
149
|
-
value = round(calc_emission(TERM_ID, emission_factor_mean,
|
|
150
|
-
sd = round(calc_emission(TERM_ID, emission_factor_sd,
|
|
145
|
+
def _run(emission_factor_mean: float, emission_factor_sd: float, organic_soils: float, land_occupation: float):
|
|
146
|
+
value = round(calc_emission(TERM_ID, emission_factor_mean, organic_soils, land_occupation), 6)
|
|
147
|
+
sd = round(calc_emission(TERM_ID, emission_factor_sd, organic_soils, land_occupation), 6)
|
|
151
148
|
return [_emission(value, sd)]
|
|
152
149
|
|
|
153
150
|
|
|
154
151
|
def run(cycle: dict):
|
|
155
|
-
should_run, emission_factor_mean, emission_factor_sd,
|
|
156
|
-
return _run(emission_factor_mean, emission_factor_sd,
|
|
152
|
+
should_run, emission_factor_mean, emission_factor_sd, organic_soils, land_occupation = _should_run(cycle)
|
|
153
|
+
return _run(emission_factor_mean, emission_factor_sd, organic_soils, land_occupation) if should_run else []
|