hestia-earth-models 0.75.0__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/aware/scarcityWeightedWaterUse.py +2 -4
- hestia_earth/models/aware2_0/scarcityWeightedWaterUse.py +2 -5
- hestia_earth/models/chaudharyBrooks2018/utils.py +2 -2
- hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +3 -2
- hestia_earth/models/cml2001Baseline/abioticResourceDepletionMineralsAndMetals.py +13 -6
- hestia_earth/models/config/Cycle.json +15 -0
- hestia_earth/models/cycle/product/economicValueShare.py +4 -4
- hestia_earth/models/ecoalimV9/utils.py +3 -3
- hestia_earth/models/ecoinventV3/utils.py +2 -2
- hestia_earth/models/ecoinventV3AndEmberClimate/utils.py +2 -2
- hestia_earth/models/emissionNotRelevant/__init__.py +2 -2
- hestia_earth/models/environmentalFootprintV3_1/environmentalFootprintSingleOverallScore.py +1 -2
- hestia_earth/models/environmentalFootprintV3_1/soilQualityIndexLandOccupation.py +8 -5
- hestia_earth/models/faostat2018/utils.py +3 -3
- hestia_earth/models/frischknechtEtAl2000/ionisingRadiationKbqU235Eq.py +5 -4
- hestia_earth/models/geospatialDatabase/ecoClimateZone.py +2 -2
- hestia_earth/models/geospatialDatabase/histosol.py +31 -11
- hestia_earth/models/hestia/aboveGroundCropResidueTotal.py +2 -2
- hestia_earth/models/hestia/landCover_utils.py +8 -12
- hestia_earth/models/hestia/management.py +3 -3
- hestia_earth/models/hestia/seed_emissions.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/ch4ToAirEntericFermentation.py +12 -9
- hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +28 -10
- 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/ipcc2019/pastureGrass_utils.py +6 -3
- hestia_earth/models/ipcc2019/utils.py +3 -2
- hestia_earth/models/linkedImpactAssessment/emissions.py +2 -2
- hestia_earth/models/mocking/search-results.json +1 -1
- hestia_earth/models/requirements.py +2 -2
- hestia_earth/models/utils/aggregated.py +2 -2
- hestia_earth/models/utils/background_emissions.py +6 -5
- hestia_earth/models/utils/blank_node.py +68 -0
- hestia_earth/models/utils/ecoClimateZone.py +7 -8
- hestia_earth/models/utils/excretaManagement.py +3 -3
- hestia_earth/models/utils/feedipedia.py +7 -7
- hestia_earth/models/utils/impact_assessment.py +3 -0
- hestia_earth/models/utils/input.py +2 -2
- hestia_earth/models/utils/liveAnimal.py +4 -4
- hestia_earth/models/utils/lookup.py +15 -20
- hestia_earth/models/utils/property.py +3 -3
- hestia_earth/models/utils/term.py +5 -5
- hestia_earth/models/version.py +1 -1
- hestia_earth/orchestrator/models/transformations.py +2 -2
- hestia_earth/orchestrator/strategies/merge/merge_node.py +32 -2
- {hestia_earth_models-0.75.0.dist-info → hestia_earth_models-0.75.2.dist-info}/METADATA +2 -2
- {hestia_earth_models-0.75.0.dist-info → hestia_earth_models-0.75.2.dist-info}/RECORD +58 -57
- {hestia_earth_models-0.75.0.dist-info → hestia_earth_models-0.75.2.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.75.0.dist-info → hestia_earth_models-0.75.2.dist-info}/licenses/LICENSE +0 -0
- {hestia_earth_models-0.75.0.dist-info → hestia_earth_models-0.75.2.dist-info}/top_level.txt +0 -0
|
@@ -200,6 +200,7 @@ def _run_practice(
|
|
|
200
200
|
GE = (
|
|
201
201
|
calculate_GE([values], REM, REG, NEwool, NEm_feed, NEg_feed) / (meanDE/100)
|
|
202
202
|
) if meanDE else 0
|
|
203
|
+
has_positive_GE_value = GE >= 0
|
|
203
204
|
|
|
204
205
|
value = (GE / meanECHHV) * (list_sum(practice.get('value', [0])) / 100)
|
|
205
206
|
|
|
@@ -229,11 +230,12 @@ def _run_practice(
|
|
|
229
230
|
logRequirements(cycle, model=MODEL, term=input_term_id, animalId=animal.get('animalId'), model_key=MODEL_KEY,
|
|
230
231
|
feed_logs=log_as_table(log_feed),
|
|
231
232
|
has_positive_feed_values=has_positive_feed_values,
|
|
233
|
+
has_positive_GE_value=has_positive_GE_value,
|
|
232
234
|
animal_logs=logs,
|
|
233
235
|
animal_lookups=animal_lookups,
|
|
234
236
|
animal_properties=animal_properties)
|
|
235
237
|
|
|
236
|
-
should_run = all([has_positive_feed_values])
|
|
238
|
+
should_run = all([has_positive_feed_values, has_positive_GE_value])
|
|
237
239
|
logShouldRun(cycle, MODEL, input_term_id, should_run, animalId=animal.get('animalId'), model_key=MODEL_KEY)
|
|
238
240
|
|
|
239
241
|
return _input(input_term_id, value) if should_run else None
|
|
@@ -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)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from hestia_earth.schema import EmissionMethodTier
|
|
2
|
-
from hestia_earth.utils.lookup import
|
|
2
|
+
from hestia_earth.utils.lookup import download_lookup, get_table_value, extract_grouped_data
|
|
3
3
|
from hestia_earth.utils.model import find_primary_product, find_term_match
|
|
4
4
|
from hestia_earth.utils.tools import list_sum, safe_parse_float
|
|
5
5
|
|
|
@@ -168,9 +168,12 @@ def _extract_groupped_data(value: str, DE: float, NDF: float, ionophore: bool, m
|
|
|
168
168
|
|
|
169
169
|
def _get_lookup_value(lookup, term: dict, lookup_col: str, DE: float, NDF: float, ionophore: bool, milk_yield: float):
|
|
170
170
|
term_id = term.get('@id')
|
|
171
|
-
value = get_table_value(lookup, '
|
|
172
|
-
debugMissingLookup(f"{term.get('termType')}.csv", '
|
|
173
|
-
return
|
|
171
|
+
value = get_table_value(lookup, 'term.id', term_id, lookup_col) if term_id else None
|
|
172
|
+
debugMissingLookup(f"{term.get('termType')}.csv", 'term.id', term_id, lookup_col, value, model=MODEL, term=TERM_ID)
|
|
173
|
+
return (
|
|
174
|
+
value if value is None or not isinstance(value, str) else
|
|
175
|
+
_extract_groupped_data(value, DE, NDF, ionophore, milk_yield)
|
|
176
|
+
)
|
|
174
177
|
|
|
175
178
|
|
|
176
179
|
def _get_milk_yield(cycle: dict):
|
|
@@ -180,8 +183,8 @@ def _get_milk_yield(cycle: dict):
|
|
|
180
183
|
|
|
181
184
|
def _get_DE_type(lookup, term_id: str, term_type: str):
|
|
182
185
|
lookup_col = LOOKUPS.get(term_type, [None])[0]
|
|
183
|
-
value = get_table_value(lookup, '
|
|
184
|
-
debugMissingLookup(f"{term_type}.csv", '
|
|
186
|
+
value = get_table_value(lookup, 'term.id', term_id, lookup_col) if lookup_col else None
|
|
187
|
+
debugMissingLookup(f"{term_type}.csv", 'term.id', term_id, lookup_col, value, model=MODEL, term=TERM_ID)
|
|
185
188
|
return value
|
|
186
189
|
|
|
187
190
|
|
|
@@ -202,9 +205,9 @@ def _is_ionophore(cycle: dict, total_feed: float):
|
|
|
202
205
|
|
|
203
206
|
def _get_default_values(lookup, term: dict):
|
|
204
207
|
term_id = term.get('@id')
|
|
205
|
-
value = get_table_value(lookup, '
|
|
206
|
-
min = get_table_value(lookup, '
|
|
207
|
-
max = get_table_value(lookup, '
|
|
208
|
+
value = get_table_value(lookup, 'term.id', term_id, LOOKUPS['liveAnimal'][3]) if term_id else None
|
|
209
|
+
min = get_table_value(lookup, 'term.id', term_id, LOOKUPS['liveAnimal'][4]) if term_id else None
|
|
210
|
+
max = get_table_value(lookup, 'term.id', term_id, LOOKUPS['liveAnimal'][5]) if term_id else None
|
|
208
211
|
return {
|
|
209
212
|
'value': safe_parse_float(value, default=None),
|
|
210
213
|
'min': safe_parse_float(min, default=None),
|
|
@@ -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": [{
|
|
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):
|
|
@@ -94,8 +102,8 @@ def _get_excreta_b0(country: dict, input: dict):
|
|
|
94
102
|
def _get_excretaManagement_MCF_from_lookup(term_id: str, ecoClimateZone: int, duration_key: DURATION):
|
|
95
103
|
lookup_name = 'excretaManagement-ecoClimateZone-CH4conv.csv'
|
|
96
104
|
lookup = download_lookup(lookup_name)
|
|
97
|
-
data_values = get_table_value(lookup, '
|
|
98
|
-
debugMissingLookup(lookup_name, '
|
|
105
|
+
data_values = get_table_value(lookup, 'term.id', term_id, str(ecoClimateZone))
|
|
106
|
+
debugMissingLookup(lookup_name, 'term.id', term_id, ecoClimateZone, data_values, model=MODEL, term=TERM_ID)
|
|
99
107
|
return safe_parse_float(
|
|
100
108
|
extract_grouped_data(data_values, duration_key.value)
|
|
101
109
|
or extract_grouped_data(data_values, DEFAULT_DURATION.value), # defaults to 12 months if no duration specified
|
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
253
|
+
organic_soils: float,
|
|
257
254
|
land_occupation: float
|
|
258
255
|
):
|
|
259
|
-
land_emission = calc_emission(TERM_ID, emission_factor,
|
|
260
|
-
ditch_emission = calc_emission(TERM_ID, ditch_factor,
|
|
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)
|