hestia-earth-models 0.64.4__py3-none-any.whl → 0.64.5__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/blonkConsultants2016/ch4ToAirNaturalVegetationBurning.py +5 -9
- hestia_earth/models/blonkConsultants2016/co2ToAirAboveGroundBiomassStockChangeLandUseChange.py +5 -9
- hestia_earth/models/blonkConsultants2016/n2OToAirNaturalVegetationBurningDirect.py +6 -13
- hestia_earth/models/cycle/animal/input/properties.py +6 -0
- hestia_earth/models/cycle/completeness/soilAmendment.py +3 -2
- hestia_earth/models/cycle/concentrateFeed.py +10 -4
- hestia_earth/models/cycle/input/properties.py +6 -0
- hestia_earth/models/cycle/liveAnimal.py +2 -2
- hestia_earth/models/cycle/milkYield.py +3 -3
- hestia_earth/models/cycle/otherSitesArea.py +59 -0
- hestia_earth/models/cycle/otherSitesUnusedDuration.py +9 -8
- hestia_earth/models/cycle/pastureSystem.py +3 -2
- hestia_earth/models/cycle/product/properties.py +6 -0
- hestia_earth/models/cycle/siteArea.py +83 -0
- hestia_earth/models/cycle/stockingDensityAnimalHousingAverage.py +28 -16
- hestia_earth/models/cycle/utils.py +1 -1
- hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandOccupation.py +128 -0
- hestia_earth/models/environmentalFootprintV3/utils.py +17 -0
- hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +17 -6
- hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +17 -6
- hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +904 -0
- hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +70 -618
- hestia_earth/models/mocking/search-results.json +395 -323
- hestia_earth/models/pooreNemecek2018/saplings.py +10 -7
- hestia_earth/models/site/management.py +18 -14
- hestia_earth/models/utils/__init__.py +38 -0
- hestia_earth/models/utils/array_builders.py +63 -52
- hestia_earth/models/utils/blank_node.py +137 -82
- hestia_earth/models/utils/descriptive_stats.py +3 -239
- hestia_earth/models/utils/feedipedia.py +15 -2
- hestia_earth/models/utils/landCover.py +9 -0
- hestia_earth/models/utils/lookup.py +13 -2
- hestia_earth/models/utils/measurement.py +3 -28
- hestia_earth/models/utils/stats.py +429 -0
- hestia_earth/models/utils/term.py +15 -3
- hestia_earth/models/utils/time_series.py +90 -0
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/METADATA +1 -1
- {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/RECORD +62 -48
- tests/models/blonkConsultants2016/test_ch4ToAirNaturalVegetationBurning.py +2 -2
- tests/models/blonkConsultants2016/test_co2ToAirAboveGroundBiomassStockChangeLandUseChange.py +2 -2
- tests/models/blonkConsultants2016/test_n2OToAirNaturalVegetationBurningDirect.py +2 -2
- tests/models/cycle/completeness/test_soilAmendment.py +1 -1
- tests/models/cycle/test_liveAnimal.py +1 -1
- tests/models/cycle/test_milkYield.py +1 -1
- tests/models/cycle/test_otherSitesArea.py +68 -0
- tests/models/cycle/test_siteArea.py +51 -0
- tests/models/cycle/test_stockingDensityAnimalHousingAverage.py +2 -2
- tests/models/environmentalFootprintV3/test_soilQualityIndexLandOccupation.py +136 -0
- tests/models/ipcc2019/test_co2ToAirCarbonStockChange_utils.py +50 -0
- tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +1 -39
- tests/models/pooreNemecek2018/test_saplings.py +1 -1
- tests/models/site/test_management.py +3 -153
- tests/models/utils/test_array_builders.py +67 -6
- tests/models/utils/test_blank_node.py +191 -7
- tests/models/utils/test_descriptive_stats.py +2 -86
- tests/models/utils/test_measurement.py +1 -22
- tests/models/utils/test_stats.py +186 -0
- tests/models/utils/test_time_series.py +88 -0
- {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for calculating CO2 emissions based on changes in carbon stocks (e.g., `organicCarbonPerHa`,
|
|
3
|
+
`aboveGroundBiomass` and `belowGroundBiomass`).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from functools import reduce
|
|
9
|
+
from itertools import product
|
|
10
|
+
from numpy import array, random, mean
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
from pydash.objects import merge
|
|
13
|
+
from typing import NamedTuple, Optional, Union
|
|
14
|
+
|
|
15
|
+
from hestia_earth.schema import EmissionMethodTier, MeasurementMethodClassification
|
|
16
|
+
from hestia_earth.utils.date import diff_in_days, YEAR
|
|
17
|
+
from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
|
|
18
|
+
|
|
19
|
+
from hestia_earth.models.log import log_as_table
|
|
20
|
+
from hestia_earth.models.utils import pairwise
|
|
21
|
+
from hestia_earth.models.utils.array_builders import correlated_normal_2d
|
|
22
|
+
from hestia_earth.models.utils.blank_node import (
|
|
23
|
+
_gapfill_datestr, DatestrGapfillMode, group_nodes_by_year, split_node_by_dates
|
|
24
|
+
)
|
|
25
|
+
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
26
|
+
from hestia_earth.models.utils.emission import min_emission_method_tier
|
|
27
|
+
from hestia_earth.models.utils.measurement import (
|
|
28
|
+
group_measurements_by_method_classification, min_measurement_method_classification,
|
|
29
|
+
to_measurement_method_classification
|
|
30
|
+
)
|
|
31
|
+
from hestia_earth.models.utils.time_series import (
|
|
32
|
+
calc_tau, compute_time_series_correlation_matrix, exponential_decay
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_MAX_CORRELATION = 1
|
|
36
|
+
_MIN_CORRELATION = 0.5
|
|
37
|
+
_NOMINAL_ERROR = 75
|
|
38
|
+
"""
|
|
39
|
+
carbon stock measurements without an associated `sd` should be assigned a nominal error of 75% (2*sd as a percentage of
|
|
40
|
+
the mean).
|
|
41
|
+
"""
|
|
42
|
+
_TRANSITION_PERIOD = 20 * YEAR # 20 years in days
|
|
43
|
+
_VALID_MEASUREMENT_METHOD_CLASSIFICATIONS = [
|
|
44
|
+
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
|
|
45
|
+
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
|
|
46
|
+
MeasurementMethodClassification.TIER_3_MODEL,
|
|
47
|
+
MeasurementMethodClassification.TIER_2_MODEL,
|
|
48
|
+
MeasurementMethodClassification.TIER_1_MODEL
|
|
49
|
+
]
|
|
50
|
+
"""
|
|
51
|
+
The list of `MeasurementMethodClassification`s that can be used to calculate carbon stock change emissions, ranked in
|
|
52
|
+
order from strongest to weakest.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _InventoryKey(Enum):
|
|
57
|
+
"""
|
|
58
|
+
The inner keys of the annualised inventory created by the `_compile_inventory` function.
|
|
59
|
+
|
|
60
|
+
The value of each enum member is formatted to be used as a column header in the `log_as_table` function.
|
|
61
|
+
"""
|
|
62
|
+
CARBON_STOCK = "carbon-stock"
|
|
63
|
+
CARBON_STOCK_CHANGE = "carbon-stock-change"
|
|
64
|
+
CO2_EMISSION = "carbon-emission"
|
|
65
|
+
SHARE_OF_EMISSION = "share-of-emissions"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
CarbonStock = NamedTuple("CarbonStock", [
|
|
69
|
+
("value", NDArray),
|
|
70
|
+
("date", str),
|
|
71
|
+
("method", MeasurementMethodClassification)
|
|
72
|
+
])
|
|
73
|
+
"""
|
|
74
|
+
NamedTuple representing a carbon stock (e.g., `organicCarbonPerHa` or `aboveGroundBiomass`).
|
|
75
|
+
|
|
76
|
+
Attributes
|
|
77
|
+
----------
|
|
78
|
+
value : NDArray
|
|
79
|
+
The value of the carbon stock measurement (kg C ha-1).
|
|
80
|
+
date : str
|
|
81
|
+
The date of the measurement as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm:ss`.
|
|
82
|
+
method: MeasurementMethodClassification
|
|
83
|
+
The measurement method for the carbon stock.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
CarbonStockChange = NamedTuple("CarbonStockChange", [
|
|
88
|
+
("value", NDArray),
|
|
89
|
+
("start_date", str),
|
|
90
|
+
("end_date", str),
|
|
91
|
+
("method", MeasurementMethodClassification)
|
|
92
|
+
])
|
|
93
|
+
"""
|
|
94
|
+
NamedTuple representing a carbon stock change.
|
|
95
|
+
|
|
96
|
+
Attributes
|
|
97
|
+
----------
|
|
98
|
+
value : NDArray
|
|
99
|
+
The value of the carbon stock change (kg C ha-1).
|
|
100
|
+
start_date : str
|
|
101
|
+
The start date of the carbon stock change event as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
102
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
103
|
+
end_date : str
|
|
104
|
+
The end date of the carbon stock change event as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
105
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
106
|
+
method: MeasurementMethodClassification
|
|
107
|
+
The measurement method for the carbon stock change.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
CarbonStockChangeEmission = NamedTuple("CarbonStockChangeEmission", [
|
|
112
|
+
("value", NDArray),
|
|
113
|
+
("start_date", str),
|
|
114
|
+
("end_date", str),
|
|
115
|
+
("method", EmissionMethodTier)
|
|
116
|
+
])
|
|
117
|
+
"""
|
|
118
|
+
NamedTuple representing a carbon stock change emission.
|
|
119
|
+
|
|
120
|
+
Attributes
|
|
121
|
+
----------
|
|
122
|
+
value : NDArray
|
|
123
|
+
The value of the carbon stock change emission (kg CO2 ha-1).
|
|
124
|
+
start_date : str
|
|
125
|
+
The start date of the carbon stock change emission as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
126
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
127
|
+
end_date : str
|
|
128
|
+
The end date of the carbon stock change emission as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
129
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
130
|
+
method: MeasurementMethodClassification
|
|
131
|
+
The emission method tier.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def lerp_carbon_stocks(start: CarbonStock, end: CarbonStock, target_date: str) -> CarbonStock:
|
|
136
|
+
"""
|
|
137
|
+
Estimate, using linear interpolation, a carbon stock for a specific date based on the carbon stocks of two other
|
|
138
|
+
dates.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
start : CarbonStock
|
|
143
|
+
The `CarbonStock` at the start (kg C ha-1).
|
|
144
|
+
end : CarbonStock
|
|
145
|
+
The `CarbonStock` at the end (kg C ha-1).
|
|
146
|
+
target_date : str
|
|
147
|
+
The target date for interpolation as a datestr with format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
148
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
CarbonStock
|
|
153
|
+
The interpolated `CarbonStock` for the specified date (kg C ha-1).
|
|
154
|
+
"""
|
|
155
|
+
alpha = diff_in_days(start.date, target_date) / diff_in_days(start.date, end.date)
|
|
156
|
+
value = (1 - alpha) * start.value + alpha * end.value
|
|
157
|
+
method = min_measurement_method_classification(start.method, end.method)
|
|
158
|
+
return CarbonStock(value, target_date, method)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def calc_carbon_stock_change(start: CarbonStock, end: CarbonStock) -> CarbonStockChange:
|
|
162
|
+
"""
|
|
163
|
+
Calculate the change in a carbon stock between two different dates.
|
|
164
|
+
|
|
165
|
+
The method should be the weaker of the two `MeasurementMethodClassification`s.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
start : CarbonStock
|
|
170
|
+
The carbon stock at the start (kg C ha-1).
|
|
171
|
+
end : CarbonStock
|
|
172
|
+
The carbon stock at the end (kg C ha-1).
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
CarbonStockChange
|
|
177
|
+
The carbon stock change (kg C ha-1).
|
|
178
|
+
"""
|
|
179
|
+
value = end.value - start.value
|
|
180
|
+
method = min_measurement_method_classification(start.method, end.method)
|
|
181
|
+
return CarbonStockChange(value, start.date, end.date, method)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def calc_carbon_stock_change_emission(carbon_stock_change: CarbonStockChange) -> CarbonStockChangeEmission:
|
|
185
|
+
"""
|
|
186
|
+
Convert a `CarbonStockChange` into a `CarbonStockChangeEmission`.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
carbon_stock_change : CarbonStockChange
|
|
191
|
+
The carbon stock change (kg C ha-1).
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
CarbonStockChangeEmission
|
|
196
|
+
The carbon stock change emission (kg CO2 ha-1).
|
|
197
|
+
"""
|
|
198
|
+
value = _convert_c_to_co2(carbon_stock_change.value) * -1
|
|
199
|
+
method = _convert_mmc_to_emt(carbon_stock_change.method)
|
|
200
|
+
return CarbonStockChangeEmission(value, carbon_stock_change.start_date, carbon_stock_change.end_date, method)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _convert_mmc_to_emt(
|
|
204
|
+
measurement_method_classification: MeasurementMethodClassification
|
|
205
|
+
) -> EmissionMethodTier:
|
|
206
|
+
"""
|
|
207
|
+
Get the emission method tier based on the provided measurement method classification.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
measurement_method_classification : MeasurementMethodClassification
|
|
212
|
+
The measurement method classification to convert into the corresponding emission method tier.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
EmissionMethodTier
|
|
217
|
+
The corresponding emission method tier.
|
|
218
|
+
"""
|
|
219
|
+
return _MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER.get(
|
|
220
|
+
to_measurement_method_classification(measurement_method_classification),
|
|
221
|
+
_DEFAULT_EMISSION_METHOD_TIER
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
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
|
+
def _convert_c_to_co2(kg_c: float) -> float:
|
|
240
|
+
"""
|
|
241
|
+
Convert mass of carbon (C) to carbon dioxide (CO2) using the atomic conversion ratio.
|
|
242
|
+
|
|
243
|
+
n.b. `get_atomic_conversion` returns the ratio C:CO2 (~44/12).
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
kg_c : float
|
|
248
|
+
Mass of carbon (C) to be converted to carbon dioxide (CO2) (kg C).
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
float
|
|
253
|
+
Mass of carbon dioxide (CO2) resulting from the conversion (kg CO2).
|
|
254
|
+
"""
|
|
255
|
+
return kg_c * get_atomic_conversion(Units.KG_CO2, Units.TO_C)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def rescale_carbon_stock_change_emission(
|
|
259
|
+
emission: CarbonStockChangeEmission, factor: float
|
|
260
|
+
) -> CarbonStockChangeEmission:
|
|
261
|
+
"""
|
|
262
|
+
Rescale a `CarbonStockChangeEmission` by a specified factor.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
emission : CarbonStockChangeEmission
|
|
267
|
+
A carbon stock change emission (kg CO2 ha-1).
|
|
268
|
+
factor : float
|
|
269
|
+
A scaling factor, representing a proportion of the total emission as a decimal. (e.g., a
|
|
270
|
+
[Cycles](https://www.hestia.earth/schema/Cycle)'s share of an annual emission).
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
CarbonStockChangeEmission
|
|
275
|
+
The rescaled emission.
|
|
276
|
+
"""
|
|
277
|
+
value = emission.value * factor
|
|
278
|
+
return CarbonStockChangeEmission(value, emission.start_date, emission.end_date, emission.method)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def add_carbon_stock_change_emissions(
|
|
282
|
+
emission_1: CarbonStockChangeEmission, emission_2: CarbonStockChangeEmission
|
|
283
|
+
) -> CarbonStockChangeEmission:
|
|
284
|
+
"""
|
|
285
|
+
Sum together multiple `CarbonStockChangeEmission`s.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
emission_1 : CarbonStockChangeEmission
|
|
290
|
+
A carbon stock change emission (kg CO2 ha-1).
|
|
291
|
+
emission_2 : CarbonStockChangeEmission
|
|
292
|
+
A carbon stock change emission (kg CO2 ha-1).
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
CarbonStockChangeEmission
|
|
297
|
+
The summed emission.
|
|
298
|
+
"""
|
|
299
|
+
value = emission_1.value + emission_2.value
|
|
300
|
+
start_date = min(emission_1.start_date, emission_2.start_date)
|
|
301
|
+
end_date = max(emission_1.end_date, emission_2.end_date)
|
|
302
|
+
method = min_emission_method_tier(emission_1.method, emission_2.method)
|
|
303
|
+
|
|
304
|
+
return CarbonStockChangeEmission(value, start_date, end_date, method)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def compile_inventory(
|
|
308
|
+
cycle_id: str,
|
|
309
|
+
cycles: list[dict],
|
|
310
|
+
carbon_stock_measurements: list[dict],
|
|
311
|
+
iterations: int = 10000,
|
|
312
|
+
seed: Union[int, random.Generator, None] = None
|
|
313
|
+
) -> tuple[dict, dict]:
|
|
314
|
+
"""
|
|
315
|
+
Compile an annual inventory of carbon stocks, changes in carbon stocks, carbon stock change emissions, and the share
|
|
316
|
+
of emissions of cycles based on the provided cycles and measurement data.
|
|
317
|
+
|
|
318
|
+
A separate inventory is compiled for each valid `MeasurementMethodClassification` present in the data, and the
|
|
319
|
+
strongest available method is chosen for each relevant inventory year. These inventories are then merged into one
|
|
320
|
+
final result.
|
|
321
|
+
|
|
322
|
+
The final inventory structure is:
|
|
323
|
+
```
|
|
324
|
+
{
|
|
325
|
+
year (int): {
|
|
326
|
+
_InventoryKey.CARBON_STOCK: value (CarbonStock),
|
|
327
|
+
_InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
|
|
328
|
+
_InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission),
|
|
329
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
330
|
+
cycle_id (str): value (float),
|
|
331
|
+
...cycle_ids
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
...years
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
cycle_id : str
|
|
341
|
+
The unique identifier of the cycle being processed.
|
|
342
|
+
cycles : list[dict]
|
|
343
|
+
A list of cycle data dictionaries, each representing land management events or cycles, grouped by years.
|
|
344
|
+
carbon_stock_measurements: list[dict]
|
|
345
|
+
A list of dictionaries, each representing carbon stock measurements across time and methods.
|
|
346
|
+
iterations : int, optional
|
|
347
|
+
The number of iterations for stochastic processing (default is 10,000).
|
|
348
|
+
seed : int, random.Generator, or None, optional
|
|
349
|
+
Seed for random number generation to ensure reproducibility. Default is None.
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
tuple[dict, dict]
|
|
355
|
+
`(inventory, logs)`
|
|
356
|
+
"""
|
|
357
|
+
# Process cycles and carbon stock measurements independently
|
|
358
|
+
cycle_inventory = _compile_cycle_inventory(cycles)
|
|
359
|
+
carbon_stock_inventory = _compile_carbon_stock_inventory(
|
|
360
|
+
carbon_stock_measurements, iterations=iterations, seed=seed
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Generate logs without side-effects
|
|
364
|
+
logs = _generate_logs(cycle_inventory, carbon_stock_inventory)
|
|
365
|
+
|
|
366
|
+
# Combine the inventories functionally
|
|
367
|
+
inventory = _squash_inventory(cycle_id, cycle_inventory, carbon_stock_inventory)
|
|
368
|
+
|
|
369
|
+
return inventory, logs
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _compile_cycle_inventory(cycles: list[dict]) -> dict:
|
|
373
|
+
"""
|
|
374
|
+
Calculate grouped share of emissions for cycles based on the amount they contribute the the overall land management
|
|
375
|
+
of an inventory year.
|
|
376
|
+
|
|
377
|
+
This function groups cycles by year, then calculates the share of emissions for each cycle based on the
|
|
378
|
+
`fraction_of_group_duration` value. The share of emissions is normalized by the sum of cycle occupancies for the
|
|
379
|
+
entire dataset to ensure the values represent a valid share.
|
|
380
|
+
|
|
381
|
+
The returned inventory has the shape:
|
|
382
|
+
```
|
|
383
|
+
{
|
|
384
|
+
year (int): {
|
|
385
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
386
|
+
cycle_id (str): value (float),
|
|
387
|
+
...cycle_ids
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
...more years
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
cycles : list[dict]
|
|
397
|
+
List of [Cycle nodes](https://www.hestia.earth/schema/Cycle), where each cycle dictionary should contain a
|
|
398
|
+
"fraction_of_group_duration" key added by the `group_nodes_by_year` function.
|
|
399
|
+
iterations : int, optional
|
|
400
|
+
Number of iterations for stochastic sampling when processing carbon stock values (default is 10,000).
|
|
401
|
+
seed : int, random.Generator, or None, optional
|
|
402
|
+
Seed for random number generation (default is None).
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
dict
|
|
407
|
+
A dictionary with grouped share of emissions for each cycle based on the fraction of the year.
|
|
408
|
+
"""
|
|
409
|
+
grouped_cycles = group_nodes_by_year(cycles)
|
|
410
|
+
|
|
411
|
+
def calculate_emissions(cycles_in_year):
|
|
412
|
+
total_fraction = sum(c.get("fraction_of_group_duration", 0) for c in cycles_in_year)
|
|
413
|
+
return {
|
|
414
|
+
cycle["@id"]: cycle.get("fraction_of_group_duration", 0) / total_fraction
|
|
415
|
+
for cycle in cycles_in_year
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
year: {_InventoryKey.SHARE_OF_EMISSION: calculate_emissions(cycles_in_year)}
|
|
420
|
+
for year, cycles_in_year in grouped_cycles.items()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _compile_carbon_stock_inventory(
|
|
425
|
+
carbon_stock_measurements: list[dict],
|
|
426
|
+
iterations: int = 10000,
|
|
427
|
+
seed: Union[int, random.Generator, None] = None
|
|
428
|
+
) -> dict:
|
|
429
|
+
"""
|
|
430
|
+
Compile an annual inventory of carbon stock data and pre-computed carbon stock change emissions.
|
|
431
|
+
|
|
432
|
+
Carbon stock measurements are grouped by the method used (MeasurementMethodClassification). For each method,
|
|
433
|
+
carbon stocks are processed for each year and changes between years are computed, followed by the calculation of
|
|
434
|
+
CO2 emissions.
|
|
435
|
+
|
|
436
|
+
The returned inventory has the shape:
|
|
437
|
+
```
|
|
438
|
+
{
|
|
439
|
+
method (MeasurementMethodClassification): {
|
|
440
|
+
year (int): {
|
|
441
|
+
_InventoryKey.CARBON_STOCK: value (CarbonStock),
|
|
442
|
+
_InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
|
|
443
|
+
_InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission)
|
|
444
|
+
},
|
|
445
|
+
...more years
|
|
446
|
+
}
|
|
447
|
+
...more methods
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Parameters
|
|
452
|
+
----------
|
|
453
|
+
carbon_stock_measurements : list[dict]
|
|
454
|
+
List of carbon [Measurement nodes](https://www.hestia.earth/schema/Measurement) nodes.
|
|
455
|
+
iterations : int, optional
|
|
456
|
+
Number of iterations for stochastic sampling when processing carbon stock values (default is 10,000).
|
|
457
|
+
seed : int, random.Generator, or None, optional
|
|
458
|
+
Seed for random number generation (default is None).
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
dict
|
|
463
|
+
The carbon stock inventory grouped by measurement method classification.
|
|
464
|
+
"""
|
|
465
|
+
carbon_stock_measurements_by_method = group_measurements_by_method_classification(carbon_stock_measurements)
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
method: _process_carbon_stock_measurements(measurements, iterations=iterations, seed=seed)
|
|
469
|
+
for method, measurements in carbon_stock_measurements_by_method.items()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _process_carbon_stock_measurements(
|
|
474
|
+
carbon_stock_measurements: list[dict],
|
|
475
|
+
iterations: int = 10000,
|
|
476
|
+
seed: Union[int, random.Generator, None] = None
|
|
477
|
+
) -> dict:
|
|
478
|
+
"""
|
|
479
|
+
Process carbon stock measurements to compile an annual inventory of carbon stocks, carbon stock changes, and CO2
|
|
480
|
+
emissions. The inventory is built by interpolating between measured values and calculating changes across years.
|
|
481
|
+
|
|
482
|
+
The returned inventory has the shape:
|
|
483
|
+
```
|
|
484
|
+
{
|
|
485
|
+
year (int): {
|
|
486
|
+
_InventoryKey.CARBON_STOCK: value (CarbonStock),
|
|
487
|
+
_InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
|
|
488
|
+
_InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission)
|
|
489
|
+
},
|
|
490
|
+
...more years
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
carbon_stock_measurements : list[dict]
|
|
497
|
+
List of pre-validated carbon stock [Measurement nodes](https://www.hestia.earth/schema/Measurement).
|
|
498
|
+
iterations : int, optional
|
|
499
|
+
Number of iterations for stochastic sampling when processing carbon stock values (default is 10,000).
|
|
500
|
+
seed : int, random.Generator, or None, optional
|
|
501
|
+
Seed for random number generation (default is None).
|
|
502
|
+
|
|
503
|
+
Returns
|
|
504
|
+
-------
|
|
505
|
+
dict
|
|
506
|
+
The annual inventory.
|
|
507
|
+
"""
|
|
508
|
+
carbon_stocks = _preprocess_carbon_stocks(carbon_stock_measurements, iterations, seed)
|
|
509
|
+
|
|
510
|
+
carbon_stocks_by_year = _interpolate_carbon_stocks(carbon_stocks)
|
|
511
|
+
carbon_stock_changes_by_year = _calculate_stock_changes(carbon_stocks_by_year)
|
|
512
|
+
co2_emissions_by_year = _calculate_co2_emissions(carbon_stock_changes_by_year)
|
|
513
|
+
|
|
514
|
+
return _sorted_merge(carbon_stocks_by_year, carbon_stock_changes_by_year, co2_emissions_by_year)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _preprocess_carbon_stocks(
|
|
518
|
+
carbon_stock_measurements: list[dict],
|
|
519
|
+
iterations: int = 10000,
|
|
520
|
+
seed: Union[int, random.Generator, None] = None
|
|
521
|
+
) -> list[CarbonStock]:
|
|
522
|
+
"""
|
|
523
|
+
Pre-process a list of carbon stock measurements by normalizing and sorting them by date. The measurements are used
|
|
524
|
+
to create correlated samples using stochastic sampling methods.
|
|
525
|
+
|
|
526
|
+
The carbon stock measurements are processed to fill in any gaps in data (e.g., missing standard deviations), and
|
|
527
|
+
correlated samples are drawn to handle measurement uncertainty.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
carbon_stock_measurements : list[dict]
|
|
532
|
+
List of pre-validated carbon stock [Measurement nodes](https://www.hestia.earth/schema/Measurement).
|
|
533
|
+
iterations : int, optional
|
|
534
|
+
Number of iterations for stochastic sampling when processing carbon stock values (default is 10,000).
|
|
535
|
+
seed : int, random.Generator, or None, optional
|
|
536
|
+
Seed for random number generation (default is None).
|
|
537
|
+
|
|
538
|
+
Returns
|
|
539
|
+
-------
|
|
540
|
+
list[CarbonStock]
|
|
541
|
+
A list of carbon stocks sorted by date.
|
|
542
|
+
"""
|
|
543
|
+
sorted_measurements = sorted(
|
|
544
|
+
flatten([split_node_by_dates(m) for m in carbon_stock_measurements]),
|
|
545
|
+
key=lambda node: _gapfill_datestr(node["dates"][0], DatestrGapfillMode.END)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
values = flatten(node["value"] for node in sorted_measurements)
|
|
549
|
+
|
|
550
|
+
sds = flatten(
|
|
551
|
+
node.get("sd", []) or [_calc_nominal_sd(v, _NOMINAL_ERROR) for v in node["value"]]
|
|
552
|
+
for node in sorted_measurements
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
dates = flatten(
|
|
556
|
+
[_gapfill_datestr(datestr, DatestrGapfillMode.END) for datestr in node["dates"]]
|
|
557
|
+
for node in sorted_measurements
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
methods = flatten(
|
|
561
|
+
[MeasurementMethodClassification(node.get("methodClassification")) for _ in node["value"]]
|
|
562
|
+
for node in sorted_measurements
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
correlation_matrix = compute_time_series_correlation_matrix(
|
|
566
|
+
dates,
|
|
567
|
+
decay_fn=lambda dt: exponential_decay(
|
|
568
|
+
dt,
|
|
569
|
+
tau=calc_tau(_TRANSITION_PERIOD),
|
|
570
|
+
initial_value=_MAX_CORRELATION,
|
|
571
|
+
final_value=_MIN_CORRELATION
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
correlated_samples = correlated_normal_2d(
|
|
576
|
+
iterations,
|
|
577
|
+
array(values),
|
|
578
|
+
array(sds),
|
|
579
|
+
correlation_matrix,
|
|
580
|
+
seed=seed
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
return [
|
|
584
|
+
CarbonStock(value=sample, date=date, method=method)
|
|
585
|
+
for sample, date, method in zip(correlated_samples, dates, methods)
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _calc_nominal_sd(value: float, error: float) -> float:
|
|
590
|
+
"""
|
|
591
|
+
Calculate a nominal SD for a carbon stock measurement. Can be used to gap fill SD when information not present in
|
|
592
|
+
measurement node.
|
|
593
|
+
"""
|
|
594
|
+
return value * error / 200
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _interpolate_carbon_stocks(carbon_stocks: list[CarbonStock]) -> dict:
|
|
598
|
+
"""
|
|
599
|
+
Interpolate between carbon stock measurements to estimate annual carbon stocks.
|
|
600
|
+
|
|
601
|
+
The function takes a list of carbon stock measurements and interpolates between pairs of consecutive measurements
|
|
602
|
+
to estimate the carbon stock values for each year in between.
|
|
603
|
+
|
|
604
|
+
The returned dictionary has the format:
|
|
605
|
+
```
|
|
606
|
+
{
|
|
607
|
+
year (int): {
|
|
608
|
+
_InventoryKey.CARBON_STOCK: value (CarbonStock),
|
|
609
|
+
},
|
|
610
|
+
...more years
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
"""
|
|
614
|
+
def interpolate_between(result: dict, carbon_stock_pair: tuple[CarbonStock, CarbonStock]) -> dict:
|
|
615
|
+
start, end = carbon_stock_pair[0], carbon_stock_pair[1]
|
|
616
|
+
|
|
617
|
+
start_date = safe_parse_date(start.date, datetime.min)
|
|
618
|
+
end_date = safe_parse_date(end.date, datetime.min)
|
|
619
|
+
|
|
620
|
+
should_run = (
|
|
621
|
+
datetime.min != start_date != end_date
|
|
622
|
+
and end_date > start_date
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
update = {
|
|
626
|
+
year: {_InventoryKey.CARBON_STOCK: lerp_carbon_stocks(
|
|
627
|
+
start,
|
|
628
|
+
end,
|
|
629
|
+
f"{year}-12-31T23:59:59"
|
|
630
|
+
)} for year in range(start_date.year, end_date.year+1)
|
|
631
|
+
} if should_run else {}
|
|
632
|
+
|
|
633
|
+
return result | update
|
|
634
|
+
|
|
635
|
+
return reduce(interpolate_between, pairwise(carbon_stocks), dict())
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _calculate_stock_changes(carbon_stocks_by_year: dict) -> dict:
|
|
639
|
+
"""
|
|
640
|
+
Calculate the change in carbon stock between consecutive years.
|
|
641
|
+
|
|
642
|
+
The function takes a dictionary of carbon stock values keyed by year and computes the difference between the
|
|
643
|
+
carbon stock for each year and the previous year. The result is stored as a `CarbonStockChange` object.
|
|
644
|
+
|
|
645
|
+
The returned dictionary has the format:
|
|
646
|
+
```
|
|
647
|
+
{
|
|
648
|
+
year (int): {
|
|
649
|
+
_InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
|
|
650
|
+
},
|
|
651
|
+
...more years
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
"""
|
|
655
|
+
return {
|
|
656
|
+
year: {
|
|
657
|
+
_InventoryKey.CARBON_STOCK_CHANGE: calc_carbon_stock_change(
|
|
658
|
+
start_group[_InventoryKey.CARBON_STOCK],
|
|
659
|
+
end_group[_InventoryKey.CARBON_STOCK]
|
|
660
|
+
)
|
|
661
|
+
} for (_, start_group), (year, end_group) in pairwise(carbon_stocks_by_year.items())
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _calculate_co2_emissions(carbon_stock_changes_by_year: dict) -> dict:
|
|
666
|
+
"""
|
|
667
|
+
Calculate CO2 emissions from changes in carbon stock between consecutive years.
|
|
668
|
+
|
|
669
|
+
The function takes a dictionary of carbon stock changes and calculates the corresponding CO2 emissions for each
|
|
670
|
+
year using a predefined emission factor.
|
|
671
|
+
|
|
672
|
+
The returned dictionary has the format:
|
|
673
|
+
```
|
|
674
|
+
{
|
|
675
|
+
year (int): {
|
|
676
|
+
_InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission),
|
|
677
|
+
},
|
|
678
|
+
...more years
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
"""
|
|
682
|
+
return {
|
|
683
|
+
year: {
|
|
684
|
+
_InventoryKey.CO2_EMISSION: calc_carbon_stock_change_emission(
|
|
685
|
+
group[_InventoryKey.CARBON_STOCK_CHANGE]
|
|
686
|
+
)
|
|
687
|
+
} for year, group in carbon_stock_changes_by_year.items()
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _sorted_merge(*sources: Union[dict, list[dict]]) -> dict:
|
|
692
|
+
"""
|
|
693
|
+
Merge one or more dictionaries into a single dictionary, ensuring that the keys are sorted in temporal order.
|
|
694
|
+
|
|
695
|
+
Parameters
|
|
696
|
+
----------
|
|
697
|
+
*sources : dict | list[dict]
|
|
698
|
+
One or more dictionaries or lists of dictionaries to be merged.
|
|
699
|
+
|
|
700
|
+
Returns
|
|
701
|
+
-------
|
|
702
|
+
dict
|
|
703
|
+
A new dictionary containing the merged key-value pairs, with keys sorted.
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
_sources = non_empty_list(
|
|
707
|
+
flatten([arg if isinstance(arg, list) else [arg] for arg in sources])
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
merged = reduce(merge, _sources, {})
|
|
711
|
+
return dict(sorted(merged.items()))
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _squash_inventory(cycle_id: str, cycle_inventory: dict, carbon_stock_inventory: dict) -> dict:
|
|
715
|
+
"""
|
|
716
|
+
Combine the `cycle_inventory` and `carbon_stock_inventory` into a single inventory by merging data for each year
|
|
717
|
+
using the strongest available `MeasurementMethodClassification`. Any years not relevant to the cycle identified
|
|
718
|
+
by `cycle_id` are excluded.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
cycle_id : str
|
|
723
|
+
The unique identifier of the cycle being processed.
|
|
724
|
+
cycle_inventory : dict
|
|
725
|
+
A dictionary representing the share of emissions for each cycle, grouped by year.
|
|
726
|
+
Format:
|
|
727
|
+
```
|
|
728
|
+
{
|
|
729
|
+
year (int): {
|
|
730
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
731
|
+
cycle_id (str): value (float),
|
|
732
|
+
...other cycle_ids
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
...more years
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
carbon_stock_inventory : dict
|
|
739
|
+
A dictionary representing carbon stock and emissions data grouped by measurement method and year.
|
|
740
|
+
Format:
|
|
741
|
+
```
|
|
742
|
+
{
|
|
743
|
+
method (MeasurementMethodClassification): {
|
|
744
|
+
year (int): {
|
|
745
|
+
_InventoryKey.CARBON_STOCK: value (CarbonStock),
|
|
746
|
+
_InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
|
|
747
|
+
_InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission)
|
|
748
|
+
},
|
|
749
|
+
...more years
|
|
750
|
+
},
|
|
751
|
+
...more methods
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Returns
|
|
756
|
+
-------
|
|
757
|
+
dict
|
|
758
|
+
A combined inventory that merges cycle and carbon stock inventories for relevant years and cycles.
|
|
759
|
+
The resulting structure is:
|
|
760
|
+
```
|
|
761
|
+
{
|
|
762
|
+
year (int): {
|
|
763
|
+
_InventoryKey.CARBON_STOCK: value (CarbonStock),
|
|
764
|
+
_InventoryKey.CARBON_STOCK_CHANGE: value (CarbonStockChange),
|
|
765
|
+
_InventoryKey.CO2_EMISSION: value (CarbonStockChangeEmission),
|
|
766
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
767
|
+
cycle_id (str): value (float),
|
|
768
|
+
...other cycle_ids
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
...more years
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
"""
|
|
775
|
+
inventory_years = sorted(set(non_empty_list(
|
|
776
|
+
flatten(list(years) for years in carbon_stock_inventory.values())
|
|
777
|
+
+ list(cycle_inventory.keys())
|
|
778
|
+
)))
|
|
779
|
+
|
|
780
|
+
def should_run_group(method: MeasurementMethodClassification, year: int) -> bool:
|
|
781
|
+
carbon_stock_inventory_group = carbon_stock_inventory.get(method, {}).get(year, {})
|
|
782
|
+
share_of_emissions_group = cycle_inventory.get(year, {})
|
|
783
|
+
|
|
784
|
+
has_emission = _InventoryKey.CO2_EMISSION in carbon_stock_inventory_group.keys()
|
|
785
|
+
is_relevant_for_cycle = cycle_id in share_of_emissions_group.get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
|
|
786
|
+
return all([has_emission, is_relevant_for_cycle])
|
|
787
|
+
|
|
788
|
+
def squash(result: dict, year: int) -> dict:
|
|
789
|
+
update_dict = next(
|
|
790
|
+
(
|
|
791
|
+
{year: reduce(merge, [carbon_stock_inventory[method][year], cycle_inventory[year]], dict())}
|
|
792
|
+
for method in _VALID_MEASUREMENT_METHOD_CLASSIFICATIONS if should_run_group(method, year)
|
|
793
|
+
),
|
|
794
|
+
{}
|
|
795
|
+
)
|
|
796
|
+
return result | update_dict
|
|
797
|
+
|
|
798
|
+
return reduce(squash, inventory_years, dict())
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _generate_logs(cycle_inventory: dict, carbon_stock_inventory: dict) -> dict:
|
|
802
|
+
"""
|
|
803
|
+
Generate logs for the compiled inventory, providing details about cycle and carbon inventories.
|
|
804
|
+
|
|
805
|
+
Parameters
|
|
806
|
+
----------
|
|
807
|
+
cycle_inventory : dict
|
|
808
|
+
The compiled cycle inventory.
|
|
809
|
+
carbon_stock_inventory : dict
|
|
810
|
+
The compiled carbon stock inventory.
|
|
811
|
+
|
|
812
|
+
Returns
|
|
813
|
+
-------
|
|
814
|
+
dict
|
|
815
|
+
A dictionary containing formatted log entries for cycle and carbon inventories.
|
|
816
|
+
"""
|
|
817
|
+
logs = {
|
|
818
|
+
"cycle_inventory": _format_cycle_inventory(cycle_inventory),
|
|
819
|
+
"carbon_stock_inventory": _format_carbon_stock_inventory(carbon_stock_inventory),
|
|
820
|
+
}
|
|
821
|
+
return logs
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _format_cycle_inventory(cycle_inventory: dict) -> str:
|
|
825
|
+
"""
|
|
826
|
+
Format the cycle inventory for logging as a table. Rows represent inventory years, columns represent the share of
|
|
827
|
+
emission for each cycle present in the inventory. If the inventory is invalid, return `"None"` as a string.
|
|
828
|
+
"""
|
|
829
|
+
KEY = _InventoryKey.SHARE_OF_EMISSION
|
|
830
|
+
|
|
831
|
+
unique_cycles = sorted(
|
|
832
|
+
set(non_empty_list(flatten(list(group[KEY]) for group in cycle_inventory.values()))),
|
|
833
|
+
key=lambda id: next((year, id) for year in cycle_inventory if id in cycle_inventory[year][KEY])
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
should_run = cycle_inventory and len(unique_cycles) > 0
|
|
837
|
+
|
|
838
|
+
return log_as_table(
|
|
839
|
+
{
|
|
840
|
+
"year": year,
|
|
841
|
+
**{
|
|
842
|
+
id: _format_number(group.get(KEY, {}).get(id, 0)) for id in unique_cycles
|
|
843
|
+
}
|
|
844
|
+
} for year, group in cycle_inventory.items()
|
|
845
|
+
) if should_run else "None"
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _format_carbon_stock_inventory(carbon_stock_inventory: dict) -> str:
|
|
849
|
+
"""
|
|
850
|
+
Format the carbon stock inventory for logging as a table. Rows represent inventory years, columns represent carbon
|
|
851
|
+
stock change data for each measurement method classification present in inventory. If the inventory is invalid,
|
|
852
|
+
return `"None"` as a string.
|
|
853
|
+
"""
|
|
854
|
+
KEYS = [
|
|
855
|
+
_InventoryKey.CARBON_STOCK,
|
|
856
|
+
_InventoryKey.CARBON_STOCK_CHANGE,
|
|
857
|
+
_InventoryKey.CO2_EMISSION
|
|
858
|
+
]
|
|
859
|
+
|
|
860
|
+
methods = carbon_stock_inventory.keys()
|
|
861
|
+
method_columns = list(product(methods, KEYS))
|
|
862
|
+
inventory_years = sorted(set(non_empty_list(flatten(list(years) for years in carbon_stock_inventory.values()))))
|
|
863
|
+
|
|
864
|
+
should_run = carbon_stock_inventory and len(inventory_years) > 0
|
|
865
|
+
|
|
866
|
+
return log_as_table(
|
|
867
|
+
{
|
|
868
|
+
"year": year,
|
|
869
|
+
**{
|
|
870
|
+
_format_column_header(method, key): _format_named_tuple(
|
|
871
|
+
carbon_stock_inventory.get(method, {}).get(year, {}).get(key, {})
|
|
872
|
+
) for method, key in method_columns
|
|
873
|
+
}
|
|
874
|
+
} for year in inventory_years
|
|
875
|
+
) if should_run else "None"
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _format_number(value: Optional[float]) -> str:
|
|
879
|
+
"""Format a float for logging in a table. If the value is invalid, return `"None"` as a string."""
|
|
880
|
+
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _format_column_header(method: MeasurementMethodClassification, inventory_key: _InventoryKey) -> str:
|
|
884
|
+
"""
|
|
885
|
+
Format a measurement method classification and inventory key for logging in a table as a column header. Replace any
|
|
886
|
+
whitespaces in the method value with dashes and concatenate it with the inventory key value, which already has the
|
|
887
|
+
correct format.
|
|
888
|
+
"""
|
|
889
|
+
return "-".join([
|
|
890
|
+
method.value.replace(" ", "-"),
|
|
891
|
+
inventory_key.value
|
|
892
|
+
])
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, CarbonStockChangeEmission]]) -> str:
|
|
896
|
+
"""
|
|
897
|
+
Format a named tuple (`CarbonStock`, `CarbonStockChange` or `CarbonStockChangeEmission`) for logging in a table.
|
|
898
|
+
Extract and format just the value and discard the other data. If the value is invalid, return `"None"` as a string.
|
|
899
|
+
"""
|
|
900
|
+
return (
|
|
901
|
+
_format_number(mean(value.value))
|
|
902
|
+
if isinstance(value, (CarbonStock, CarbonStockChange, CarbonStockChangeEmission))
|
|
903
|
+
else "None"
|
|
904
|
+
)
|