hestia-earth-models 0.64.6__py3-none-any.whl → 0.64.8__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/cycle/animal/milkYield.py +10 -22
- hestia_earth/models/cycle/unknownPreSeasonWaterRegime.py +0 -1
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandOccupation.py +25 -24
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandTransformation.py +182 -0
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexTotalLandUseEffects.py +66 -0
- hestia_earth/models/environmentalFootprintV3/utils.py +1 -1
- hestia_earth/models/hyde32/utils.py +4 -0
- hestia_earth/models/ipcc2019/animal/pastureGrass.py +3 -1
- hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChangeLandUseChange.py +191 -0
- hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChangeLandUseChange.py +204 -0
- hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +255 -35
- hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +63 -149
- hestia_earth/models/ipcc2019/pastureGrass.py +3 -1
- hestia_earth/models/mocking/search-results.json +337 -319
- hestia_earth/models/pooreNemecek2018/landOccupationDuringCycle.py +12 -8
- hestia_earth/models/site/management.py +3 -5
- hestia_earth/models/transformation/input/excreta.py +9 -13
- hestia_earth/models/utils/input.py +5 -2
- hestia_earth/models/utils/site.py +4 -2
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.64.6.dist-info → hestia_earth_models-0.64.8.dist-info}/METADATA +2 -2
- {hestia_earth_models-0.64.6.dist-info → hestia_earth_models-0.64.8.dist-info}/RECORD +37 -29
- tests/models/cycle/animal/test_milkYield.py +1 -14
- tests/models/environmentalFootprintV3/test_freshwaterEcotoxicityPotentialCtue.py +4 -2
- tests/models/environmentalFootprintV3/test_soilQualityIndexLandOccupation.py +16 -24
- tests/models/environmentalFootprintV3/test_soilQualityIndexLandTransformation.py +113 -0
- tests/models/environmentalFootprintV3/test_soilQualityIndexTotalLandUseEffects.py +50 -0
- tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChangeLandUseChange.py +83 -0
- tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChangeLandUseChange.py +83 -0
- tests/models/ipcc2019/test_co2ToAirCarbonStockChange_utils.py +6 -6
- tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +5 -4
- tests/models/pooreNemecek2018/test_landOccupationDuringCycle.py +4 -1
- tests/models/site/test_management.py +4 -1
- tests/models/utils/test_input.py +65 -1
- {hestia_earth_models-0.64.6.dist-info → hestia_earth_models-0.64.8.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.64.6.dist-info → hestia_earth_models-0.64.8.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.64.6.dist-info → hestia_earth_models-0.64.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from hestia_earth.schema import CycleFunctionalUnit, EmissionMethodTier, SiteSiteType
|
|
2
|
+
|
|
3
|
+
from hestia_earth.models.log import logRequirements, logShouldRun
|
|
4
|
+
from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match
|
|
5
|
+
from hestia_earth.models.utils.emission import _new_emission
|
|
6
|
+
|
|
7
|
+
from .co2ToAirCarbonStockChange_utils import create_run_function, create_should_run_function
|
|
8
|
+
from . import MODEL
|
|
9
|
+
|
|
10
|
+
REQUIREMENTS = {
|
|
11
|
+
"Cycle": {
|
|
12
|
+
"site": {
|
|
13
|
+
"measurements": [
|
|
14
|
+
{
|
|
15
|
+
"@type": "Measurement",
|
|
16
|
+
"value": "",
|
|
17
|
+
"dates": "",
|
|
18
|
+
"depthUpper": "0",
|
|
19
|
+
"depthLower": "30",
|
|
20
|
+
"term.@id": " belowGroundBiomass"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"functionalUnit": "1 ha",
|
|
25
|
+
"endDate": "",
|
|
26
|
+
"optional": {
|
|
27
|
+
"startDate": ""
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
RETURNS = {
|
|
32
|
+
"Emission": [{
|
|
33
|
+
"value": "",
|
|
34
|
+
"sd": "",
|
|
35
|
+
"min": "",
|
|
36
|
+
"max": "",
|
|
37
|
+
"statsDefinition": "simulated",
|
|
38
|
+
"observations": "",
|
|
39
|
+
"methodTier": "",
|
|
40
|
+
"depth": "30"
|
|
41
|
+
}]
|
|
42
|
+
}
|
|
43
|
+
TERM_ID = 'co2ToAirBelowGroundBiomassStockChangeLandUseChange'
|
|
44
|
+
|
|
45
|
+
_DEPTH_UPPER = 0
|
|
46
|
+
_DEPTH_LOWER = 30
|
|
47
|
+
|
|
48
|
+
_CARBON_STOCK_TERM_ID = 'belowGroundBiomass'
|
|
49
|
+
|
|
50
|
+
_SITE_TYPE_SYSTEMS_MAPPING = {
|
|
51
|
+
SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value: [
|
|
52
|
+
"protectedCroppingSystemSoilBased",
|
|
53
|
+
"protectedCroppingSystemSoilAndSubstrateBased"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _emission(
|
|
59
|
+
*,
|
|
60
|
+
value: list[float],
|
|
61
|
+
method_tier: EmissionMethodTier,
|
|
62
|
+
sd: list[float] = None,
|
|
63
|
+
min: list[float] = None,
|
|
64
|
+
max: list[float] = None,
|
|
65
|
+
statsDefinition: str = None,
|
|
66
|
+
observations: list[int] = None
|
|
67
|
+
) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Create an emission node based on the provided value and method tier.
|
|
70
|
+
|
|
71
|
+
See [Emission schema](https://www.hestia.earth/schema/Emission) for more information.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
value : float
|
|
76
|
+
The emission value (kg CO2 ha-1).
|
|
77
|
+
sd : float
|
|
78
|
+
The standard deviation (kg CO2 ha-1).
|
|
79
|
+
method_tier : EmissionMethodTier
|
|
80
|
+
The emission method tier.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
dict
|
|
85
|
+
The emission dictionary with keys 'depth', 'value', and 'methodTier'.
|
|
86
|
+
"""
|
|
87
|
+
update_dict = {
|
|
88
|
+
"value": value,
|
|
89
|
+
"sd": sd,
|
|
90
|
+
"min": min,
|
|
91
|
+
"max": max,
|
|
92
|
+
"statsDefinition": statsDefinition,
|
|
93
|
+
"observations": observations,
|
|
94
|
+
"methodTier": method_tier.value,
|
|
95
|
+
"depth": _DEPTH_LOWER
|
|
96
|
+
}
|
|
97
|
+
emission = _new_emission(TERM_ID, MODEL) | {
|
|
98
|
+
key: value for key, value in update_dict.items() if value
|
|
99
|
+
}
|
|
100
|
+
return emission
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run(cycle: dict) -> list[dict]:
|
|
104
|
+
"""
|
|
105
|
+
Run the `ipcc2019.co2ToAirBelowGroundBiomassStockChangeManagementChange`.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
cycle : dict
|
|
110
|
+
A HESTIA (Cycle node)[https://www.hestia.earth/schema/Cycle].
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
list[dict]
|
|
115
|
+
A list of [Emission nodes](https://www.hestia.earth/schema/Emission) containing model results.
|
|
116
|
+
"""
|
|
117
|
+
should_run_exec = create_should_run_function(
|
|
118
|
+
_CARBON_STOCK_TERM_ID,
|
|
119
|
+
_should_compile_inventory_func,
|
|
120
|
+
should_run_measurement_func=_should_run_measurement_func
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
run_exec = create_run_function(_emission)
|
|
124
|
+
|
|
125
|
+
should_run, cycle_id, inventory, logs = should_run_exec(cycle)
|
|
126
|
+
|
|
127
|
+
logRequirements(cycle, model=MODEL, term=TERM_ID, **logs)
|
|
128
|
+
logShouldRun(cycle, MODEL, TERM_ID, should_run)
|
|
129
|
+
|
|
130
|
+
return run_exec(cycle_id, inventory) if should_run else []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _should_run_measurement_func(node: dict) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Validate a [Measurement](https://www.hestia.earth/schema/Measurement) to determine whether it is a valid
|
|
136
|
+
`organicCarbonPerHa` node.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
node : dict
|
|
141
|
+
The node to be validated.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
bool
|
|
146
|
+
`True` if the node passes all validation criteria, `False` otherwise.
|
|
147
|
+
"""
|
|
148
|
+
return all([
|
|
149
|
+
node.get("depthLower") == _DEPTH_LOWER,
|
|
150
|
+
node.get("depthUpper") == _DEPTH_UPPER
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _should_compile_inventory_func(
|
|
155
|
+
site: dict, cycles: list[dict], carbon_stock_measurements: list[dict]
|
|
156
|
+
) -> tuple[bool, dict]:
|
|
157
|
+
"""
|
|
158
|
+
Determine whether a site is suitable and has enough data to compile a carbon stock inventory.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
site : dict
|
|
163
|
+
A HESTIA (Site node)[https://www.hestia.earth/schema/Site]
|
|
164
|
+
cycles : list[dict]
|
|
165
|
+
A list of HESTIA (Cycle nodes)[https://www.hestia.earth/schema/Cycle] that are related to the site.
|
|
166
|
+
carbon_stock_measurements : list[dict]
|
|
167
|
+
A list of HESTIA carbon stock (Measurement nodes)[https://www.hestia.earth/schema/Measurement] that are related
|
|
168
|
+
to the site.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
tuple[bool, dict]
|
|
173
|
+
`(should_run, logs)`.
|
|
174
|
+
"""
|
|
175
|
+
site_type = site.get("siteType")
|
|
176
|
+
has_soil = site_type not in _SITE_TYPE_SYSTEMS_MAPPING or all(
|
|
177
|
+
cumulative_nodes_term_match(
|
|
178
|
+
cycle.get("practices", []),
|
|
179
|
+
target_term_ids=_SITE_TYPE_SYSTEMS_MAPPING[site_type],
|
|
180
|
+
cumulative_threshold=0
|
|
181
|
+
) for cycle in cycles
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
has_stock_measurements = len(carbon_stock_measurements) > 0
|
|
185
|
+
has_cycles = len(cycles) > 0
|
|
186
|
+
has_functional_unit_1_ha = all(cycle.get('functionalUnit') == CycleFunctionalUnit._1_HA.value for cycle in cycles)
|
|
187
|
+
|
|
188
|
+
should_run = all([
|
|
189
|
+
has_soil,
|
|
190
|
+
has_stock_measurements,
|
|
191
|
+
has_cycles,
|
|
192
|
+
has_functional_unit_1_ha
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
logs = {
|
|
196
|
+
"site_type": site_type,
|
|
197
|
+
"has_soil": has_soil,
|
|
198
|
+
"carbon_stock_term": _CARBON_STOCK_TERM_ID,
|
|
199
|
+
"has_stock_measurements": has_stock_measurements,
|
|
200
|
+
"has_cycles": has_cycles,
|
|
201
|
+
"has_functional_unit_1_ha": has_functional_unit_1_ha,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return should_run, logs
|
|
@@ -10,28 +10,36 @@ from itertools import product
|
|
|
10
10
|
from numpy import array, random, mean
|
|
11
11
|
from numpy.typing import NDArray
|
|
12
12
|
from pydash.objects import merge
|
|
13
|
-
from typing import NamedTuple, Optional, Union
|
|
13
|
+
from typing import Callable, NamedTuple, Optional, Union
|
|
14
14
|
|
|
15
|
-
from hestia_earth.schema import
|
|
15
|
+
from hestia_earth.schema import (
|
|
16
|
+
EmissionMethodTier, EmissionStatsDefinition, MeasurementMethodClassification
|
|
17
|
+
)
|
|
16
18
|
from hestia_earth.utils.date import diff_in_days, YEAR
|
|
17
19
|
from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
|
|
18
20
|
|
|
19
21
|
from hestia_earth.models.log import log_as_table
|
|
20
22
|
from hestia_earth.models.utils import pairwise
|
|
21
|
-
from hestia_earth.models.utils.array_builders import correlated_normal_2d
|
|
23
|
+
from hestia_earth.models.utils.array_builders import correlated_normal_2d, gen_seed
|
|
22
24
|
from hestia_earth.models.utils.blank_node import (
|
|
23
|
-
_gapfill_datestr, DatestrGapfillMode, group_nodes_by_year,
|
|
25
|
+
_gapfill_datestr, _get_datestr_format, DatestrGapfillMode, DatestrFormat, group_nodes_by_year, node_term_match,
|
|
26
|
+
split_node_by_dates
|
|
24
27
|
)
|
|
25
28
|
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
29
|
+
from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
|
|
26
30
|
from hestia_earth.models.utils.emission import min_emission_method_tier
|
|
27
31
|
from hestia_earth.models.utils.measurement import (
|
|
28
32
|
group_measurements_by_method_classification, min_measurement_method_classification,
|
|
29
33
|
to_measurement_method_classification
|
|
30
34
|
)
|
|
35
|
+
from hestia_earth.models.utils.site import related_cycles
|
|
31
36
|
from hestia_earth.models.utils.time_series import (
|
|
32
37
|
calc_tau, compute_time_series_correlation_matrix, exponential_decay
|
|
33
38
|
)
|
|
34
39
|
|
|
40
|
+
from .utils import check_consecutive
|
|
41
|
+
|
|
42
|
+
_ITERATIONS = 10000
|
|
35
43
|
_MAX_CORRELATION = 1
|
|
36
44
|
_MIN_CORRELATION = 0.5
|
|
37
45
|
_NOMINAL_ERROR = 75
|
|
@@ -40,7 +48,14 @@ carbon stock measurements without an associated `sd` should be assigned a nomina
|
|
|
40
48
|
the mean).
|
|
41
49
|
"""
|
|
42
50
|
_TRANSITION_PERIOD = 20 * YEAR # 20 years in days
|
|
43
|
-
|
|
51
|
+
_VALID_DATE_FORMATS = {
|
|
52
|
+
DatestrFormat.YEAR,
|
|
53
|
+
DatestrFormat.YEAR_MONTH,
|
|
54
|
+
DatestrFormat.YEAR_MONTH_DAY,
|
|
55
|
+
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
DEFAULT_MEASUREMENT_METHOD_RANKING = [
|
|
44
59
|
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
|
|
45
60
|
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
|
|
46
61
|
MeasurementMethodClassification.TIER_3_MODEL,
|
|
@@ -52,6 +67,19 @@ The list of `MeasurementMethodClassification`s that can be used to calculate car
|
|
|
52
67
|
order from strongest to weakest.
|
|
53
68
|
"""
|
|
54
69
|
|
|
70
|
+
_DEFAULT_EMISSION_METHOD_TIER = EmissionMethodTier.TIER_1
|
|
71
|
+
_MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER = {
|
|
72
|
+
MeasurementMethodClassification.TIER_2_MODEL: EmissionMethodTier.TIER_2,
|
|
73
|
+
MeasurementMethodClassification.TIER_3_MODEL: EmissionMethodTier.TIER_3,
|
|
74
|
+
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS: EmissionMethodTier.MEASURED,
|
|
75
|
+
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT: EmissionMethodTier.MEASURED,
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
A mapping between `MeasurementMethodClassification`s and `EmissionMethodTier`s. As carbon stock measurements can be
|
|
79
|
+
measured/estimated through a variety of methods, the emission model needs be able to assign an emission tier for each.
|
|
80
|
+
Any `MeasurementMethodClassification` not in the mapping should be assigned `DEFAULT_EMISSION_METHOD_TIER`.
|
|
81
|
+
"""
|
|
82
|
+
|
|
55
83
|
|
|
56
84
|
class _InventoryKey(Enum):
|
|
57
85
|
"""
|
|
@@ -132,7 +160,7 @@ method: MeasurementMethodClassification
|
|
|
132
160
|
"""
|
|
133
161
|
|
|
134
162
|
|
|
135
|
-
def
|
|
163
|
+
def _lerp_carbon_stocks(start: CarbonStock, end: CarbonStock, target_date: str) -> CarbonStock:
|
|
136
164
|
"""
|
|
137
165
|
Estimate, using linear interpolation, a carbon stock for a specific date based on the carbon stocks of two other
|
|
138
166
|
dates.
|
|
@@ -158,7 +186,7 @@ def lerp_carbon_stocks(start: CarbonStock, end: CarbonStock, target_date: str) -
|
|
|
158
186
|
return CarbonStock(value, target_date, method)
|
|
159
187
|
|
|
160
188
|
|
|
161
|
-
def
|
|
189
|
+
def _calc_carbon_stock_change(start: CarbonStock, end: CarbonStock) -> CarbonStockChange:
|
|
162
190
|
"""
|
|
163
191
|
Calculate the change in a carbon stock between two different dates.
|
|
164
192
|
|
|
@@ -181,7 +209,7 @@ def calc_carbon_stock_change(start: CarbonStock, end: CarbonStock) -> CarbonStoc
|
|
|
181
209
|
return CarbonStockChange(value, start.date, end.date, method)
|
|
182
210
|
|
|
183
211
|
|
|
184
|
-
def
|
|
212
|
+
def _calc_carbon_stock_change_emission(carbon_stock_change: CarbonStockChange) -> CarbonStockChangeEmission:
|
|
185
213
|
"""
|
|
186
214
|
Convert a `CarbonStockChange` into a `CarbonStockChangeEmission`.
|
|
187
215
|
|
|
@@ -222,20 +250,6 @@ def _convert_mmc_to_emt(
|
|
|
222
250
|
)
|
|
223
251
|
|
|
224
252
|
|
|
225
|
-
_DEFAULT_EMISSION_METHOD_TIER = EmissionMethodTier.TIER_1
|
|
226
|
-
_MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER = {
|
|
227
|
-
MeasurementMethodClassification.TIER_2_MODEL: EmissionMethodTier.TIER_2,
|
|
228
|
-
MeasurementMethodClassification.TIER_3_MODEL: EmissionMethodTier.TIER_3,
|
|
229
|
-
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS: EmissionMethodTier.MEASURED,
|
|
230
|
-
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT: EmissionMethodTier.MEASURED,
|
|
231
|
-
}
|
|
232
|
-
"""
|
|
233
|
-
A mapping between `MeasurementMethodClassification`s and `EmissionMethodTier`s. As carbon stock measurements can be
|
|
234
|
-
measured/estimated through a variety of methods, the emission model needs be able to assign an emission tier for each.
|
|
235
|
-
Any `MeasurementMethodClassification` not in the mapping should be assigned `DEFAULT_EMISSION_METHOD_TIER`.
|
|
236
|
-
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
253
|
def _convert_c_to_co2(kg_c: float) -> float:
|
|
240
254
|
"""
|
|
241
255
|
Convert mass of carbon (C) to carbon dioxide (CO2) using the atomic conversion ratio.
|
|
@@ -255,7 +269,7 @@ def _convert_c_to_co2(kg_c: float) -> float:
|
|
|
255
269
|
return kg_c * get_atomic_conversion(Units.KG_CO2, Units.TO_C)
|
|
256
270
|
|
|
257
271
|
|
|
258
|
-
def
|
|
272
|
+
def _rescale_carbon_stock_change_emission(
|
|
259
273
|
emission: CarbonStockChangeEmission, factor: float
|
|
260
274
|
) -> CarbonStockChangeEmission:
|
|
261
275
|
"""
|
|
@@ -278,7 +292,7 @@ def rescale_carbon_stock_change_emission(
|
|
|
278
292
|
return CarbonStockChangeEmission(value, emission.start_date, emission.end_date, emission.method)
|
|
279
293
|
|
|
280
294
|
|
|
281
|
-
def
|
|
295
|
+
def _add_carbon_stock_change_emissions(
|
|
282
296
|
emission_1: CarbonStockChangeEmission, emission_2: CarbonStockChangeEmission
|
|
283
297
|
) -> CarbonStockChangeEmission:
|
|
284
298
|
"""
|
|
@@ -304,12 +318,157 @@ def add_carbon_stock_change_emissions(
|
|
|
304
318
|
return CarbonStockChangeEmission(value, start_date, end_date, method)
|
|
305
319
|
|
|
306
320
|
|
|
307
|
-
def
|
|
321
|
+
def create_should_run_function(
|
|
322
|
+
carbon_stock_term_id: str,
|
|
323
|
+
should_compile_inventory_func: Callable[[dict, list[dict], list[dict]], tuple[bool, dict]],
|
|
324
|
+
should_run_measurement_func: Callable[[dict], bool] = lambda _: True,
|
|
325
|
+
measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING
|
|
326
|
+
) -> Callable[[dict], tuple[bool, str, dict, dict]]:
|
|
327
|
+
"""
|
|
328
|
+
Create a should run function for an emissions from carbon stock change model.
|
|
329
|
+
|
|
330
|
+
Model-specific validation functions should be passed as parameters to this higher order function to determine which
|
|
331
|
+
carbon stock measurements are included in the inventory and whether there is enough data to compile an annual
|
|
332
|
+
inventory of carbon stock change data.
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
carbon_stock_term_id : str
|
|
337
|
+
The `term.@id` of the carbon stock measurement (e.g., `aboveGroundBiomass`, `belowGroundBiomass`,
|
|
338
|
+
`organicCarbonPerHa`, etc.).
|
|
339
|
+
|
|
340
|
+
should_compile_inventory_func : Callable[[dict, list[dict], list[dict]], tuple[bool, dict]]
|
|
341
|
+
A function, with the signature
|
|
342
|
+
`(site: dict, cycles: list[dict], carbon_stock_measurements: list[dict]) -> (should_run: bool, logs: dict)`, to
|
|
343
|
+
determine whether there is enough site and cycles data available to compile the carbon stock change inventory.
|
|
344
|
+
|
|
345
|
+
should_run_measurement_func : Callable[[dict], bool], optional.
|
|
346
|
+
An optional measurement validation function, with the signature `(measurement: dict) -> bool`, that can be used
|
|
347
|
+
to add in additional criteria (`depthUpper`, `depthLower`, etc.) for the inclusion of a measurement in the
|
|
348
|
+
inventory.
|
|
349
|
+
|
|
350
|
+
measurement_method_ranking : list[MeasurementMethodClassification], optional
|
|
351
|
+
The order in which to prioritise `MeasurementMethodClassification`s when reducing the inventory down to a
|
|
352
|
+
single method per year. Defaults to:
|
|
353
|
+
```
|
|
354
|
+
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
|
|
355
|
+
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
|
|
356
|
+
MeasurementMethodClassification.TIER_3_MODEL,
|
|
357
|
+
MeasurementMethodClassification.TIER_2_MODEL,
|
|
358
|
+
MeasurementMethodClassification.TIER_1_MODEL
|
|
359
|
+
```
|
|
360
|
+
n.b., measurements with methods not included in the ranking will not be included in the inventory.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
Callable[[dict], tuple[bool, str, dict, dict]]
|
|
365
|
+
The customised `should_run` function with the signature
|
|
366
|
+
`(cycle: dict) -> (should_run_: bool, cycle_id: str, inventory: dict, logs: dict)`.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
def should_run(cycle: dict) -> tuple[bool, str, dict, dict]:
|
|
370
|
+
"""
|
|
371
|
+
Determine if calculations should run for a given [Cycle](https://www.hestia.earth/schema/Cycle) based on
|
|
372
|
+
available carbon stock data. If data availability is sufficient, return an inventory of pre-processed input
|
|
373
|
+
data for the model and log data.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
cycle : dict
|
|
378
|
+
The cycle dictionary for which the calculations will be evaluated.
|
|
379
|
+
|
|
380
|
+
Returns
|
|
381
|
+
-------
|
|
382
|
+
tuple[bool, str, dict, dict]
|
|
383
|
+
`(should_run, cycle_id, inventory, logs)`
|
|
384
|
+
"""
|
|
385
|
+
cycle_id = cycle.get("@id")
|
|
386
|
+
site = _get_site(cycle)
|
|
387
|
+
cycles = related_cycles(site)
|
|
388
|
+
|
|
389
|
+
carbon_stock_measurements = [
|
|
390
|
+
node for node in site.get("measurements", []) if all([
|
|
391
|
+
node_term_match(node, carbon_stock_term_id),
|
|
392
|
+
_has_valid_array_fields(node),
|
|
393
|
+
_has_valid_dates(node),
|
|
394
|
+
node.get("methodClassification") in (m.value for m in measurement_method_ranking),
|
|
395
|
+
should_run_measurement_func(node)
|
|
396
|
+
])
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
seed = gen_seed(site) # All cycles linked to the same site should be consistent
|
|
400
|
+
rng = random.default_rng(seed)
|
|
401
|
+
|
|
402
|
+
should_compile_inventory, should_compile_logs = should_compile_inventory_func(
|
|
403
|
+
site, cycles, carbon_stock_measurements
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
inventory, inventory_logs = (
|
|
407
|
+
_compile_inventory(
|
|
408
|
+
cycle_id,
|
|
409
|
+
cycles,
|
|
410
|
+
carbon_stock_measurements,
|
|
411
|
+
iterations=_ITERATIONS,
|
|
412
|
+
seed=rng,
|
|
413
|
+
measurement_method_ranking=measurement_method_ranking
|
|
414
|
+
) if should_compile_inventory else ({}, {})
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
has_valid_inventory = len(inventory) > 0
|
|
418
|
+
has_consecutive_years = check_consecutive(inventory.keys())
|
|
419
|
+
|
|
420
|
+
should_run_ = all([has_valid_inventory, has_consecutive_years])
|
|
421
|
+
|
|
422
|
+
logs = should_compile_logs | inventory_logs | {
|
|
423
|
+
"seed": seed,
|
|
424
|
+
"has_valid_inventory": has_valid_inventory,
|
|
425
|
+
"has_consecutive_years": has_consecutive_years
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return should_run_, cycle_id, inventory, logs
|
|
429
|
+
|
|
430
|
+
return should_run
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _has_valid_array_fields(node: dict) -> bool:
|
|
434
|
+
"""Validate that the array-type fields of a node (`value`, `dates`, `sd`) have data and matching lengths."""
|
|
435
|
+
value = node.get("value", [])
|
|
436
|
+
sd = node.get("sd", [])
|
|
437
|
+
dates = node.get("dates", [])
|
|
438
|
+
return all([
|
|
439
|
+
len(value) > 0,
|
|
440
|
+
len(value) == len(dates),
|
|
441
|
+
len(sd) == 0 or len(sd) == len(value)
|
|
442
|
+
])
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _has_valid_dates(node: dict) -> bool:
|
|
446
|
+
"""Validate that all dates in a node's `dates` field have a valid format."""
|
|
447
|
+
return all(_get_datestr_format(datestr) in _VALID_DATE_FORMATS for datestr in node.get("dates", []))
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _get_site(cycle: dict) -> dict:
|
|
451
|
+
"""
|
|
452
|
+
Get the [Site](https://www.hestia.earth/schema/Site) data from a [Cycle](https://www.hestia.earth/schema/Cycle).
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
cycle : dict
|
|
457
|
+
|
|
458
|
+
Returns
|
|
459
|
+
-------
|
|
460
|
+
str
|
|
461
|
+
"""
|
|
462
|
+
return cycle.get("site", {})
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _compile_inventory(
|
|
308
466
|
cycle_id: str,
|
|
309
467
|
cycles: list[dict],
|
|
310
468
|
carbon_stock_measurements: list[dict],
|
|
311
469
|
iterations: int = 10000,
|
|
312
|
-
seed: Union[int, random.Generator, None] = None
|
|
470
|
+
seed: Union[int, random.Generator, None] = None,
|
|
471
|
+
measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING
|
|
313
472
|
) -> tuple[dict, dict]:
|
|
314
473
|
"""
|
|
315
474
|
Compile an annual inventory of carbon stocks, changes in carbon stocks, carbon stock change emissions, and the share
|
|
@@ -354,17 +513,16 @@ def compile_inventory(
|
|
|
354
513
|
tuple[dict, dict]
|
|
355
514
|
`(inventory, logs)`
|
|
356
515
|
"""
|
|
357
|
-
# Process cycles and carbon stock measurements independently
|
|
358
516
|
cycle_inventory = _compile_cycle_inventory(cycles)
|
|
359
517
|
carbon_stock_inventory = _compile_carbon_stock_inventory(
|
|
360
518
|
carbon_stock_measurements, iterations=iterations, seed=seed
|
|
361
519
|
)
|
|
362
520
|
|
|
363
|
-
# Generate logs without side-effects
|
|
364
521
|
logs = _generate_logs(cycle_inventory, carbon_stock_inventory)
|
|
365
522
|
|
|
366
|
-
|
|
367
|
-
|
|
523
|
+
inventory = _squash_inventory(
|
|
524
|
+
cycle_id, cycle_inventory, carbon_stock_inventory, measurement_method_ranking=measurement_method_ranking
|
|
525
|
+
)
|
|
368
526
|
|
|
369
527
|
return inventory, logs
|
|
370
528
|
|
|
@@ -623,7 +781,7 @@ def _interpolate_carbon_stocks(carbon_stocks: list[CarbonStock]) -> dict:
|
|
|
623
781
|
)
|
|
624
782
|
|
|
625
783
|
update = {
|
|
626
|
-
year: {_InventoryKey.CARBON_STOCK:
|
|
784
|
+
year: {_InventoryKey.CARBON_STOCK: _lerp_carbon_stocks(
|
|
627
785
|
start,
|
|
628
786
|
end,
|
|
629
787
|
f"{year}-12-31T23:59:59"
|
|
@@ -654,7 +812,7 @@ def _calculate_stock_changes(carbon_stocks_by_year: dict) -> dict:
|
|
|
654
812
|
"""
|
|
655
813
|
return {
|
|
656
814
|
year: {
|
|
657
|
-
_InventoryKey.CARBON_STOCK_CHANGE:
|
|
815
|
+
_InventoryKey.CARBON_STOCK_CHANGE: _calc_carbon_stock_change(
|
|
658
816
|
start_group[_InventoryKey.CARBON_STOCK],
|
|
659
817
|
end_group[_InventoryKey.CARBON_STOCK]
|
|
660
818
|
)
|
|
@@ -681,7 +839,7 @@ def _calculate_co2_emissions(carbon_stock_changes_by_year: dict) -> dict:
|
|
|
681
839
|
"""
|
|
682
840
|
return {
|
|
683
841
|
year: {
|
|
684
|
-
_InventoryKey.CO2_EMISSION:
|
|
842
|
+
_InventoryKey.CO2_EMISSION: _calc_carbon_stock_change_emission(
|
|
685
843
|
group[_InventoryKey.CARBON_STOCK_CHANGE]
|
|
686
844
|
)
|
|
687
845
|
} for year, group in carbon_stock_changes_by_year.items()
|
|
@@ -711,7 +869,12 @@ def _sorted_merge(*sources: Union[dict, list[dict]]) -> dict:
|
|
|
711
869
|
return dict(sorted(merged.items()))
|
|
712
870
|
|
|
713
871
|
|
|
714
|
-
def _squash_inventory(
|
|
872
|
+
def _squash_inventory(
|
|
873
|
+
cycle_id: str,
|
|
874
|
+
cycle_inventory: dict,
|
|
875
|
+
carbon_stock_inventory: dict,
|
|
876
|
+
measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING
|
|
877
|
+
) -> dict:
|
|
715
878
|
"""
|
|
716
879
|
Combine the `cycle_inventory` and `carbon_stock_inventory` into a single inventory by merging data for each year
|
|
717
880
|
using the strongest available `MeasurementMethodClassification`. Any years not relevant to the cycle identified
|
|
@@ -789,7 +952,7 @@ def _squash_inventory(cycle_id: str, cycle_inventory: dict, carbon_stock_invento
|
|
|
789
952
|
update_dict = next(
|
|
790
953
|
(
|
|
791
954
|
{year: reduce(merge, [carbon_stock_inventory[method][year], cycle_inventory[year]], dict())}
|
|
792
|
-
for method in
|
|
955
|
+
for method in measurement_method_ranking if should_run_group(method, year)
|
|
793
956
|
),
|
|
794
957
|
{}
|
|
795
958
|
)
|
|
@@ -902,3 +1065,60 @@ def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, Ca
|
|
|
902
1065
|
if isinstance(value, (CarbonStock, CarbonStockChange, CarbonStockChangeEmission))
|
|
903
1066
|
else "None"
|
|
904
1067
|
)
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def create_run_function(
|
|
1071
|
+
new_emission_func: Callable[[EmissionMethodTier, dict], dict]
|
|
1072
|
+
) -> Callable[[str, dict], list[dict]]:
|
|
1073
|
+
"""
|
|
1074
|
+
Create a run function for an emissions from carbon stock change model.
|
|
1075
|
+
|
|
1076
|
+
A model-specific `new_emission_func` should be passed as a parameter to this higher-order function to control how
|
|
1077
|
+
model ouputs are formatted into HESTIA emission nodes.
|
|
1078
|
+
|
|
1079
|
+
Parameters
|
|
1080
|
+
----------
|
|
1081
|
+
new_emission_func : Callable[[EmissionMethodTier, tuple], dict]
|
|
1082
|
+
A function, with the signature `(method_tier: dict, **kwargs: dict) -> (emission_node: dict)`.
|
|
1083
|
+
|
|
1084
|
+
Returns
|
|
1085
|
+
-------
|
|
1086
|
+
Callable[[str, dict], list[dict]]
|
|
1087
|
+
The customised `run` function with the signature `(cycle_id: str, inventory: dict) -> emissions: list[dict]`.
|
|
1088
|
+
"""
|
|
1089
|
+
def run(cycle_id: str, inventory: dict) -> list[dict]:
|
|
1090
|
+
"""
|
|
1091
|
+
Calculate emissions for a specific cycle using from a carbon stock change using pre-compiled inventory data.
|
|
1092
|
+
|
|
1093
|
+
The emission method tier is based on the minimum measurement method tier of the carbon stock measures used to
|
|
1094
|
+
calculate the emission.
|
|
1095
|
+
|
|
1096
|
+
Parameters
|
|
1097
|
+
----------
|
|
1098
|
+
cycle_id : str
|
|
1099
|
+
The "@id" field of the [Cycle node](https://www.hestia.earth/schema/Cycle).
|
|
1100
|
+
grouped_data : dict
|
|
1101
|
+
A dictionary containing grouped carbon stock change and share of emissions data.
|
|
1102
|
+
|
|
1103
|
+
Returns
|
|
1104
|
+
-------
|
|
1105
|
+
list[dict]
|
|
1106
|
+
A list of [Emission nodes](https://www.hestia.earth/schema/Emission) containing model results.
|
|
1107
|
+
"""
|
|
1108
|
+
rescaled_emissions = [
|
|
1109
|
+
_rescale_carbon_stock_change_emission(
|
|
1110
|
+
group[_InventoryKey.CO2_EMISSION], group[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
|
|
1111
|
+
) for group in inventory.values()
|
|
1112
|
+
]
|
|
1113
|
+
total_emission = reduce(_add_carbon_stock_change_emissions, rescaled_emissions)
|
|
1114
|
+
|
|
1115
|
+
descriptive_stats = calc_descriptive_stats(
|
|
1116
|
+
total_emission.value,
|
|
1117
|
+
EmissionStatsDefinition.SIMULATED,
|
|
1118
|
+
decimals=6
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
method_tier = total_emission.method
|
|
1122
|
+
return [new_emission_func(method_tier=method_tier, **descriptive_stats)]
|
|
1123
|
+
|
|
1124
|
+
return run
|