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
|
@@ -1,31 +1,22 @@
|
|
|
1
|
-
from collections.abc import Iterable
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from enum import Enum
|
|
4
1
|
from functools import reduce
|
|
5
|
-
from
|
|
6
|
-
from pydash.objects import merge
|
|
7
|
-
from typing import NamedTuple, Optional, Union
|
|
2
|
+
from numpy import random
|
|
8
3
|
|
|
9
4
|
from hestia_earth.schema import (
|
|
10
|
-
CycleFunctionalUnit, EmissionMethodTier, MeasurementMethodClassification, SiteSiteType
|
|
5
|
+
CycleFunctionalUnit, EmissionMethodTier, EmissionStatsDefinition, MeasurementMethodClassification, SiteSiteType
|
|
11
6
|
)
|
|
12
|
-
from hestia_earth.utils.date import diff_in_days
|
|
13
|
-
from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
|
|
14
7
|
|
|
15
|
-
from hestia_earth.models.log import
|
|
16
|
-
from hestia_earth.models.utils import
|
|
8
|
+
from hestia_earth.models.log import logRequirements, logShouldRun
|
|
9
|
+
from hestia_earth.models.utils.array_builders import gen_seed
|
|
17
10
|
from hestia_earth.models.utils.blank_node import (
|
|
18
|
-
_get_datestr_format,
|
|
19
|
-
cumulative_nodes_term_match
|
|
20
|
-
)
|
|
21
|
-
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
22
|
-
from hestia_earth.models.utils.emission import _new_emission, min_emission_method_tier
|
|
23
|
-
from hestia_earth.models.utils.measurement import (
|
|
24
|
-
group_measurements_by_method_classification, min_measurement_method_classification,
|
|
25
|
-
to_measurement_method_classification
|
|
11
|
+
_get_datestr_format, cumulative_nodes_term_match, DatestrFormat, node_term_match
|
|
26
12
|
)
|
|
13
|
+
from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
|
|
14
|
+
from hestia_earth.models.utils.emission import _new_emission
|
|
27
15
|
from hestia_earth.models.utils.site import related_cycles
|
|
28
16
|
|
|
17
|
+
from .co2ToAirCarbonStockChange_utils import (
|
|
18
|
+
_InventoryKey, add_carbon_stock_change_emissions, compile_inventory, rescale_carbon_stock_change_emission
|
|
19
|
+
)
|
|
29
20
|
from .utils import check_consecutive
|
|
30
21
|
from . import MODEL
|
|
31
22
|
|
|
@@ -59,19 +50,20 @@ RETURNS = {
|
|
|
59
50
|
}
|
|
60
51
|
TERM_ID = 'co2ToAirSoilOrganicCarbonStockChangeManagementChange'
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
_DEPTH_UPPER = 0
|
|
54
|
+
_DEPTH_LOWER = 30
|
|
55
|
+
_ITERATIONS = 10000
|
|
64
56
|
|
|
65
|
-
|
|
57
|
+
_ORGANIC_CARBON_PER_HA_TERM_ID = 'organicCarbonPerHa'
|
|
66
58
|
|
|
67
|
-
|
|
59
|
+
_VALID_DATE_FORMATS = {
|
|
68
60
|
DatestrFormat.YEAR,
|
|
69
61
|
DatestrFormat.YEAR_MONTH,
|
|
70
62
|
DatestrFormat.YEAR_MONTH_DAY,
|
|
71
63
|
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND
|
|
72
64
|
}
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
_VALID_MEASUREMENT_METHOD_CLASSIFICATIONS = [
|
|
75
67
|
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
|
|
76
68
|
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
|
|
77
69
|
MeasurementMethodClassification.TIER_3_MODEL,
|
|
@@ -91,85 +83,16 @@ _SITE_TYPE_SYSTEMS_MAPPING = {
|
|
|
91
83
|
}
|
|
92
84
|
|
|
93
85
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
SocStock = NamedTuple("SocStock", [
|
|
107
|
-
("value", float),
|
|
108
|
-
("date", str),
|
|
109
|
-
("method", MeasurementMethodClassification)
|
|
110
|
-
])
|
|
111
|
-
"""
|
|
112
|
-
NamedTuple representing an SOC stock.
|
|
113
|
-
|
|
114
|
-
Attributes
|
|
115
|
-
----------
|
|
116
|
-
value : float
|
|
117
|
-
The value of the SOC stock (kg C ha-1).
|
|
118
|
-
date : str
|
|
119
|
-
The date of the measurement as a datestr with format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
120
|
-
`YYYY-MM-DDTHH:mm:ss`.
|
|
121
|
-
method: MeasurementMethodClassification
|
|
122
|
-
The measurement method for the SOC stock.
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
SocStockChange = NamedTuple("SocStockChange", [
|
|
126
|
-
("value", float),
|
|
127
|
-
("start_date", str),
|
|
128
|
-
("end_date", str),
|
|
129
|
-
("method", MeasurementMethodClassification)
|
|
130
|
-
])
|
|
131
|
-
"""
|
|
132
|
-
NamedTuple representing an SOC stock change.
|
|
133
|
-
|
|
134
|
-
Attributes
|
|
135
|
-
----------
|
|
136
|
-
value : float
|
|
137
|
-
The value of the SOC stock change (kg C ha-1).
|
|
138
|
-
start_date : str
|
|
139
|
-
The start date of the SOC stock change event as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
140
|
-
`YYYY-MM-DDTHH:mm:ss`.
|
|
141
|
-
end_date : str
|
|
142
|
-
The end date of the SOC stock change event as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
143
|
-
`YYYY-MM-DDTHH:mm:ss`.
|
|
144
|
-
method: MeasurementMethodClassification
|
|
145
|
-
The measurement method for the SOC stock change.
|
|
146
|
-
"""
|
|
147
|
-
|
|
148
|
-
SocStockChangeEmission = NamedTuple("SocStockChangeEmission", [
|
|
149
|
-
("value", float),
|
|
150
|
-
("start_date", str),
|
|
151
|
-
("end_date", str),
|
|
152
|
-
("method", EmissionMethodTier)
|
|
153
|
-
])
|
|
154
|
-
"""
|
|
155
|
-
NamedTuple representing an SOC stock change emission.
|
|
156
|
-
|
|
157
|
-
Attributes
|
|
158
|
-
----------
|
|
159
|
-
value : float
|
|
160
|
-
The value of the SOC stock change (kg CO2 ha-1).
|
|
161
|
-
start_date : str
|
|
162
|
-
The start date of the SOC stock change emission as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
163
|
-
`YYYY-MM-DDTHH:mm:ss`.
|
|
164
|
-
end_date : str
|
|
165
|
-
The end date of the SOC stock change emission as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
166
|
-
`YYYY-MM-DDTHH:mm:ss`.
|
|
167
|
-
method: MeasurementMethodClassification
|
|
168
|
-
The emission method tier.
|
|
169
|
-
"""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _emission(value: float, method_tier: EmissionMethodTier) -> dict:
|
|
86
|
+
def _emission(
|
|
87
|
+
*,
|
|
88
|
+
value: list[float],
|
|
89
|
+
method_tier: EmissionMethodTier,
|
|
90
|
+
sd: list[float] = None,
|
|
91
|
+
min: list[float] = None,
|
|
92
|
+
max: list[float] = None,
|
|
93
|
+
statsDefinition: str = None,
|
|
94
|
+
observations: list[int] = None
|
|
95
|
+
) -> dict:
|
|
173
96
|
"""
|
|
174
97
|
Create an emission node based on the provided value and method tier.
|
|
175
98
|
|
|
@@ -179,7 +102,8 @@ def _emission(value: float, method_tier: EmissionMethodTier) -> dict:
|
|
|
179
102
|
----------
|
|
180
103
|
value : float
|
|
181
104
|
The emission value (kg CO2 ha-1).
|
|
182
|
-
|
|
105
|
+
sd : float
|
|
106
|
+
The standard deviation (kg CO2 ha-1).
|
|
183
107
|
method_tier : EmissionMethodTier
|
|
184
108
|
The emission method tier.
|
|
185
109
|
|
|
@@ -188,10 +112,19 @@ def _emission(value: float, method_tier: EmissionMethodTier) -> dict:
|
|
|
188
112
|
dict
|
|
189
113
|
The emission dictionary with keys 'depth', 'value', and 'methodTier'.
|
|
190
114
|
"""
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
115
|
+
update_dict = {
|
|
116
|
+
"value": value,
|
|
117
|
+
"sd": sd,
|
|
118
|
+
"min": min,
|
|
119
|
+
"max": max,
|
|
120
|
+
"statsDefinition": statsDefinition,
|
|
121
|
+
"observations": observations,
|
|
122
|
+
"methodTier": method_tier.value,
|
|
123
|
+
"depth": _DEPTH_LOWER
|
|
124
|
+
}
|
|
125
|
+
emission = _new_emission(TERM_ID, MODEL) | {
|
|
126
|
+
key: value for key, value in update_dict.items() if value
|
|
127
|
+
}
|
|
195
128
|
return emission
|
|
196
129
|
|
|
197
130
|
|
|
@@ -215,6 +148,9 @@ def _should_run(cycle: dict) -> tuple[bool, str, dict]:
|
|
|
215
148
|
soc_measurements = [node for node in site.get("measurements", []) if _validate_soc_measurement(node)]
|
|
216
149
|
cycles = related_cycles(site)
|
|
217
150
|
|
|
151
|
+
seed = gen_seed(site) # All cycles linked to the same site should be consistent
|
|
152
|
+
rng = random.default_rng(seed)
|
|
153
|
+
|
|
218
154
|
site_type = site.get("siteType")
|
|
219
155
|
has_soil = site_type not in _SITE_TYPE_SYSTEMS_MAPPING or all(
|
|
220
156
|
cumulative_nodes_term_match(
|
|
@@ -236,7 +172,13 @@ def _should_run(cycle: dict) -> tuple[bool, str, dict]:
|
|
|
236
172
|
])
|
|
237
173
|
|
|
238
174
|
inventory, logs = (
|
|
239
|
-
|
|
175
|
+
compile_inventory(
|
|
176
|
+
cycle_id,
|
|
177
|
+
cycles,
|
|
178
|
+
soc_measurements,
|
|
179
|
+
iterations=_ITERATIONS,
|
|
180
|
+
seed=rng
|
|
181
|
+
) if should_compile_inventory else ({}, {})
|
|
240
182
|
)
|
|
241
183
|
|
|
242
184
|
has_valid_inventory = len(inventory) > 0
|
|
@@ -245,6 +187,7 @@ def _should_run(cycle: dict) -> tuple[bool, str, dict]:
|
|
|
245
187
|
logRequirements(
|
|
246
188
|
cycle, model=MODEL, term=TERM_ID,
|
|
247
189
|
site_type=site_type,
|
|
190
|
+
seed=seed,
|
|
248
191
|
has_soil=has_soil,
|
|
249
192
|
has_soc_measurements=has_soc_measurements,
|
|
250
193
|
has_cycles=has_cycles,
|
|
@@ -292,475 +235,20 @@ def _validate_soc_measurement(node: dict) -> bool:
|
|
|
292
235
|
`True` if the node passes all validation criteria, `False` otherwise.
|
|
293
236
|
"""
|
|
294
237
|
value = node.get("value", [])
|
|
238
|
+
sd = node.get("sd", [])
|
|
295
239
|
dates = node.get("dates", [])
|
|
296
240
|
return all([
|
|
297
|
-
node_term_match(node,
|
|
298
|
-
node.get("depthUpper") ==
|
|
299
|
-
node.get("depthLower") ==
|
|
300
|
-
node.get("methodClassification") in (m.value for m in
|
|
241
|
+
node_term_match(node, _ORGANIC_CARBON_PER_HA_TERM_ID),
|
|
242
|
+
node.get("depthUpper") == _DEPTH_UPPER,
|
|
243
|
+
node.get("depthLower") == _DEPTH_LOWER,
|
|
244
|
+
node.get("methodClassification") in (m.value for m in _VALID_MEASUREMENT_METHOD_CLASSIFICATIONS),
|
|
301
245
|
len(value) > 0,
|
|
302
246
|
len(value) == len(dates),
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def _compile_inventory(cycle_id: str, cycles: list[dict], soc_measurements: list[dict]) -> tuple[dict, dict]:
|
|
308
|
-
"""
|
|
309
|
-
Compile an annual inventory of SOC stocks, SOC stock changes, SOC stock change emissions and the share of emissions
|
|
310
|
-
of cycles.
|
|
311
|
-
|
|
312
|
-
A separate inventory is compiled for each valid `MeasurementMethodClassification` present in the input data, which
|
|
313
|
-
are then merged together by selecting the strongest available method for each relevant inventory year.
|
|
314
|
-
|
|
315
|
-
The returned inventory has the shape:
|
|
316
|
-
```
|
|
317
|
-
{
|
|
318
|
-
year (int): {
|
|
319
|
-
_InventoryKey.SOC_STOCK: value (SocStock),
|
|
320
|
-
_InventoryKey.SOC_STOCK_CHANGE: value (SocStockChange),
|
|
321
|
-
_InventoryKey.CO2_EMISSION: value (SocStockChangeEmission),
|
|
322
|
-
_InventoryKey.SHARE_OF_EMISSION: {
|
|
323
|
-
cycle_id (str): value (float),
|
|
324
|
-
...cycle_ids
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
...years
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
Parameters
|
|
332
|
-
----------
|
|
333
|
-
cycle_id : str
|
|
334
|
-
cycles : list[dict]
|
|
335
|
-
soc_measurements: list[dict]
|
|
336
|
-
|
|
337
|
-
Returns
|
|
338
|
-
-------
|
|
339
|
-
tuple[dict, dict]
|
|
340
|
-
`(inventory, logs)`
|
|
341
|
-
"""
|
|
342
|
-
cycle_inventory = _compile_cycle_inventory(cycles)
|
|
343
|
-
|
|
344
|
-
soc_measurements_by_method = group_measurements_by_method_classification(soc_measurements)
|
|
345
|
-
soc_inventory = {
|
|
346
|
-
method: _compile_soc_inventory(soc_measurements)
|
|
347
|
-
for method, soc_measurements in soc_measurements_by_method.items()
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
logs = {
|
|
351
|
-
"cycle_inventory": _format_cycle_inventory(cycle_inventory),
|
|
352
|
-
"soc_inventory": _format_soc_inventory(soc_inventory)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
inventory = _squash_inventory(cycle_id, cycle_inventory, soc_inventory)
|
|
356
|
-
return inventory, logs
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def _compile_cycle_inventory(cycles: list[dict]) -> dict:
|
|
360
|
-
"""
|
|
361
|
-
Calculate grouped share of emissions for cycles based on the amount they contribute the the overall land management
|
|
362
|
-
of an inventory year.
|
|
363
|
-
|
|
364
|
-
This function groups cycles by year, then calculates the share of emissions for each cycle based on the
|
|
365
|
-
"fraction_of_group_duration" value. The share of emissions is normalized by the sum of cycle occupancies for the
|
|
366
|
-
entire dataset to ensure the values represent a valid share.
|
|
367
|
-
|
|
368
|
-
The returned inventory has the shape:
|
|
369
|
-
```
|
|
370
|
-
{
|
|
371
|
-
year (int): {
|
|
372
|
-
_InventoryKey.SHARE_OF_EMISSION: {
|
|
373
|
-
cycle_id (str): value (float),
|
|
374
|
-
...cycle_ids
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
...years
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Parameters
|
|
382
|
-
----------
|
|
383
|
-
cycles : list[dict]
|
|
384
|
-
List of [Cycle nodes](https://www.hestia.earth/schema/Cycle), where each cycle dictionary should contain a
|
|
385
|
-
"fraction_of_group_duration" key added by the `group_nodes_by_year` function.
|
|
386
|
-
|
|
387
|
-
Returns
|
|
388
|
-
-------
|
|
389
|
-
dict
|
|
390
|
-
A dictionary with grouped share of emissions for each cycle based on the fraction of the year.
|
|
391
|
-
"""
|
|
392
|
-
grouped_cycles = group_nodes_by_year(cycles)
|
|
393
|
-
return {
|
|
394
|
-
year: {
|
|
395
|
-
_InventoryKey.SHARE_OF_EMISSION: {
|
|
396
|
-
cycle["@id"]: (
|
|
397
|
-
cycle.get("fraction_of_group_duration", 0)
|
|
398
|
-
/ sum(cycle.get("fraction_of_group_duration", 0) for cycle in cycles)
|
|
399
|
-
) for cycle in cycles
|
|
400
|
-
}
|
|
401
|
-
} for year, cycles in grouped_cycles.items()
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def _compile_soc_inventory(soc_measurements: list[dict]) -> dict:
|
|
406
|
-
"""
|
|
407
|
-
Compile an annual inventory of SOC stock data and pre-computed SOC stock change emissions.
|
|
408
|
-
|
|
409
|
-
The returned inventory has the shape:
|
|
410
|
-
```
|
|
411
|
-
{
|
|
412
|
-
year (int): {
|
|
413
|
-
_InventoryKey.SOC_STOCK: value (SocStock),
|
|
414
|
-
_InventoryKey.SOC_STOCK_CHANGE: value (SocStockChange),
|
|
415
|
-
_InventoryKey.CO2_EMISSION: value (SocStockChangeEmission)
|
|
416
|
-
},
|
|
417
|
-
...years
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Parameters
|
|
422
|
-
----------
|
|
423
|
-
soc_measurements : list[dict]
|
|
424
|
-
List of pre-validated `organicCarbonPerHa` [Measurement nodes](https://www.hestia.earth/schema/Measurement).
|
|
425
|
-
|
|
426
|
-
Returns
|
|
427
|
-
-------
|
|
428
|
-
dict
|
|
429
|
-
The annual inventory.
|
|
430
|
-
"""
|
|
431
|
-
|
|
432
|
-
values = flatten(measurement.get("value", []) for measurement in soc_measurements)
|
|
433
|
-
dates = flatten(
|
|
434
|
-
[_gapfill_datestr(datestr, DatestrGapfillMode.END) for datestr in measurement.get("dates", [])]
|
|
435
|
-
for measurement in soc_measurements
|
|
436
|
-
)
|
|
437
|
-
methods = flatten(
|
|
438
|
-
[MeasurementMethodClassification(measurement.get("methodClassification")) for _ in measurement.get("value", [])]
|
|
439
|
-
for measurement in soc_measurements
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
soc_stocks = sorted(
|
|
443
|
-
[SocStock(value, datestr, method) for value, datestr, method in zip(values, dates, methods)],
|
|
444
|
-
key=lambda soc_stock: soc_stock.date
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
def interpolate_between(result: dict, soc_stock_pair: tuple[SocStock, SocStock]) -> dict:
|
|
448
|
-
start, end = soc_stock_pair[0], soc_stock_pair[1]
|
|
449
|
-
|
|
450
|
-
start_date = safe_parse_date(start.date, datetime.min)
|
|
451
|
-
end_date = safe_parse_date(end.date, datetime.min)
|
|
452
|
-
|
|
453
|
-
should_run = (
|
|
454
|
-
datetime.min != start_date != end_date
|
|
455
|
-
and end_date > start_date
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
update = {
|
|
459
|
-
year: {_InventoryKey.SOC_STOCK: lerp_soc_stocks(start, end, f"{year}-12-31T23:59:59")}
|
|
460
|
-
for year in range(start_date.year, end_date.year+1)
|
|
461
|
-
} if should_run else {}
|
|
462
|
-
|
|
463
|
-
return result | update
|
|
464
|
-
|
|
465
|
-
soc_stocks_by_year = reduce(interpolate_between, pairwise(soc_stocks), dict())
|
|
466
|
-
|
|
467
|
-
soc_stock_changes_by_year = {
|
|
468
|
-
year: {
|
|
469
|
-
_InventoryKey.SOC_STOCK_CHANGE: calc_soc_stock_change(
|
|
470
|
-
start_group[_InventoryKey.SOC_STOCK],
|
|
471
|
-
end_group[_InventoryKey.SOC_STOCK]
|
|
472
|
-
)
|
|
473
|
-
} for (_, start_group), (year, end_group) in pairwise(soc_stocks_by_year.items())
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
co2_emissions_by_method_and_year = {
|
|
477
|
-
year: {
|
|
478
|
-
_InventoryKey.CO2_EMISSION: calc_soc_stock_change_emission(
|
|
479
|
-
group[_InventoryKey.SOC_STOCK_CHANGE]
|
|
480
|
-
)
|
|
481
|
-
} for year, group in soc_stock_changes_by_year.items()
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return _sorted_merge(soc_stocks_by_year, soc_stock_changes_by_year, co2_emissions_by_method_and_year)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def lerp_soc_stocks(start: SocStock, end: SocStock, target_date: str) -> SocStock:
|
|
488
|
-
"""
|
|
489
|
-
Estimate, using linear interpolation, an SOC stock for a specific date based on the the SOC stocks of two other
|
|
490
|
-
dates.
|
|
491
|
-
|
|
492
|
-
Parameters
|
|
493
|
-
----------
|
|
494
|
-
start : SocStock
|
|
495
|
-
The `SocStock` at the start (kg C ha-1).
|
|
496
|
-
end : SocStock
|
|
497
|
-
The `SocStock` at the end (kg C ha-1).
|
|
498
|
-
target_date : str
|
|
499
|
-
The target date for interpolation as a datestr with format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
500
|
-
`YYYY-MM-DDTHH:mm:ss`.
|
|
501
|
-
|
|
502
|
-
Returns
|
|
503
|
-
-------
|
|
504
|
-
SocStock
|
|
505
|
-
The interpolated `SocStock` for the specified date (kg C ha-1).
|
|
506
|
-
"""
|
|
507
|
-
time_ratio = diff_in_days(start.date, target_date) / diff_in_days(start.date, end.date)
|
|
508
|
-
soc_delta = (end.value - start.value) * time_ratio
|
|
509
|
-
|
|
510
|
-
value = start.value + soc_delta
|
|
511
|
-
method = min_measurement_method_classification(start.method, end.method)
|
|
512
|
-
|
|
513
|
-
return SocStock(value, target_date, method)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
def calc_soc_stock_change(start: SocStock, end: SocStock) -> SocStockChange:
|
|
517
|
-
"""
|
|
518
|
-
Calculate the change in SOC stock change between the current and previous states.
|
|
519
|
-
|
|
520
|
-
The method should be the weaker of the two `MeasurementMethodClassification`s.
|
|
521
|
-
|
|
522
|
-
Parameters
|
|
523
|
-
----------
|
|
524
|
-
start : SocStock
|
|
525
|
-
The SOC stock at the start (kg C ha-1).
|
|
526
|
-
|
|
527
|
-
end : SocStock
|
|
528
|
-
The SOC stock at the end (kg C ha-1).
|
|
529
|
-
|
|
530
|
-
Returns
|
|
531
|
-
-------
|
|
532
|
-
SocStockChange
|
|
533
|
-
The SOC stock change (kg C ha-1).
|
|
534
|
-
"""
|
|
535
|
-
value = end.value - start.value
|
|
536
|
-
method = min_measurement_method_classification(start.method, end.method)
|
|
537
|
-
return SocStockChange(value, start.date, end.date, method)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def calc_soc_stock_change_emission(soc_stock_change: SocStockChange) -> SocStockChangeEmission:
|
|
541
|
-
"""
|
|
542
|
-
Convert an `SocStockChange` into an `SocStockChangeEmission`.
|
|
543
|
-
|
|
544
|
-
Parameters
|
|
545
|
-
----------
|
|
546
|
-
soc_stock_change : SocStockChange
|
|
547
|
-
The SOC stock at the start (kg C ha-1).
|
|
548
|
-
|
|
549
|
-
Returns
|
|
550
|
-
-------
|
|
551
|
-
SocStockChangeEmission
|
|
552
|
-
The SOC stock change emission (kg CO2 ha-1).
|
|
553
|
-
"""
|
|
554
|
-
value = convert_c_to_co2(soc_stock_change.value) * -1
|
|
555
|
-
method = convert_mmc_to_emt(soc_stock_change.method)
|
|
556
|
-
return SocStockChangeEmission(value, soc_stock_change.start_date, soc_stock_change.end_date, method)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
_DEFAULT_EMISSION_METHOD_TIER = EmissionMethodTier.TIER_1
|
|
560
|
-
_MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER = {
|
|
561
|
-
MeasurementMethodClassification.TIER_2_MODEL: EmissionMethodTier.TIER_2,
|
|
562
|
-
MeasurementMethodClassification.TIER_3_MODEL: EmissionMethodTier.TIER_3,
|
|
563
|
-
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS: EmissionMethodTier.MEASURED,
|
|
564
|
-
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT: EmissionMethodTier.MEASURED,
|
|
565
|
-
}
|
|
566
|
-
"""
|
|
567
|
-
A mapping between `MeasurementMethodClassification`s and `EmissionMethodTier`s. As SOC measurements can be
|
|
568
|
-
measured/estimated through a variety of methods, the emission model needs be able to assign an emission tier for each.
|
|
569
|
-
Any `MeasurementMethodClassification` not in the mapping should be assigned `DEFAULT_EMISSION_METHOD_TIER`.
|
|
570
|
-
"""
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def convert_mmc_to_emt(
|
|
574
|
-
measurement_method_classification: MeasurementMethodClassification
|
|
575
|
-
) -> EmissionMethodTier:
|
|
576
|
-
"""
|
|
577
|
-
Get the emission method tier based on the provided measurement method classification.
|
|
578
|
-
|
|
579
|
-
Parameters
|
|
580
|
-
----------
|
|
581
|
-
measurement_method : MeasurementMethodClassification
|
|
582
|
-
The measurement method classification.
|
|
583
|
-
|
|
584
|
-
Returns
|
|
585
|
-
-------
|
|
586
|
-
EmissionMethodTier
|
|
587
|
-
The corresponding emission method tier.
|
|
588
|
-
"""
|
|
589
|
-
return _MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER.get(
|
|
590
|
-
to_measurement_method_classification(measurement_method_classification),
|
|
591
|
-
_DEFAULT_EMISSION_METHOD_TIER
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def convert_c_to_co2(kg_c: float) -> float:
|
|
596
|
-
"""
|
|
597
|
-
Convert mass of carbon (C) to carbon dioxide (CO2) using the atomic conversion ratio.
|
|
598
|
-
|
|
599
|
-
n.b. `get_atomic_conversion` returns the ratio C:CO2 (~44/12).
|
|
600
|
-
|
|
601
|
-
Parameters
|
|
602
|
-
----------
|
|
603
|
-
kg_c : float
|
|
604
|
-
Mass of carbon (C) to be converted to carbon dioxide (CO2) (kg C).
|
|
605
|
-
|
|
606
|
-
Returns
|
|
607
|
-
-------
|
|
608
|
-
float
|
|
609
|
-
Mass of carbon dioxide (CO2) resulting from the conversion (kg CO2).
|
|
610
|
-
"""
|
|
611
|
-
return kg_c * get_atomic_conversion(Units.KG_CO2, Units.TO_C)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def _sorted_merge(*sources: Union[dict, list[dict]]) -> dict:
|
|
615
|
-
"""
|
|
616
|
-
Merge dictionaries and return the result as a new dictionary with keys sorted in order to preserve the temporal
|
|
617
|
-
order of inventory years.
|
|
618
|
-
|
|
619
|
-
Parameters
|
|
620
|
-
----------
|
|
621
|
-
*sources : dict | list[dict]
|
|
622
|
-
One or more dictionaries or lists of dictionaries to be merged.
|
|
623
|
-
|
|
624
|
-
Returns
|
|
625
|
-
-------
|
|
626
|
-
dict
|
|
627
|
-
A new dictionary containing the merged key-value pairs, with keys sorted.
|
|
628
|
-
"""
|
|
629
|
-
|
|
630
|
-
_sources = non_empty_list(
|
|
631
|
-
flatten([arg if isinstance(arg, list) else [arg] for arg in sources])
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
merged = reduce(merge, _sources, {})
|
|
635
|
-
return dict(sorted(merged.items()))
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
def _squash_inventory(cycle_id: str, cycle_inventory: dict, soc_inventory: dict) -> dict:
|
|
639
|
-
"""
|
|
640
|
-
Merge the `cycle_inventory` and `soc_inventory` for each inventory year by selecting the strongest available
|
|
641
|
-
`MeasurementMethodClassification`. Years that do not overlap with the Cycle node that the emission model is running
|
|
642
|
-
on should be discarded as they are not relevant.
|
|
643
|
-
|
|
644
|
-
Parameters
|
|
645
|
-
----------
|
|
646
|
-
cycle_id : str
|
|
647
|
-
cycle_inventory : dict
|
|
648
|
-
soc_inventory: dict
|
|
649
|
-
|
|
650
|
-
Returns
|
|
651
|
-
-------
|
|
652
|
-
dict
|
|
653
|
-
The squashed inventory.
|
|
654
|
-
"""
|
|
655
|
-
inventory_years = sorted(set(non_empty_list(
|
|
656
|
-
flatten(list(years) for years in soc_inventory.values())
|
|
657
|
-
+ list(cycle_inventory.keys())
|
|
658
|
-
)))
|
|
659
|
-
|
|
660
|
-
def should_run_group(method: MeasurementMethodClassification, year: int) -> bool:
|
|
661
|
-
soc_stock_inventory_group = soc_inventory.get(method, {}).get(year, {})
|
|
662
|
-
share_of_emissions_group = cycle_inventory.get(year, {})
|
|
663
|
-
|
|
664
|
-
has_emission = _InventoryKey.CO2_EMISSION in soc_stock_inventory_group.keys()
|
|
665
|
-
is_relevant_for_cycle = cycle_id in share_of_emissions_group.get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
|
|
666
|
-
return all([has_emission, is_relevant_for_cycle])
|
|
667
|
-
|
|
668
|
-
def squash(result: dict, year: int) -> dict:
|
|
669
|
-
update_dict = next(
|
|
670
|
-
(
|
|
671
|
-
{year: reduce(merge, [soc_inventory[method][year], cycle_inventory[year]], dict())}
|
|
672
|
-
for method in VALID_MEASUREMENT_METHOD_CLASSIFICATIONS if should_run_group(method, year)
|
|
673
|
-
),
|
|
674
|
-
{}
|
|
675
|
-
)
|
|
676
|
-
return result | update_dict
|
|
677
|
-
|
|
678
|
-
return reduce(squash, inventory_years, dict())
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def _format_cycle_inventory(cycle_inventory: dict) -> str:
|
|
682
|
-
"""
|
|
683
|
-
Format the cycle inventory for logging as a table. Rows represent inventory years, columns represent the share of
|
|
684
|
-
emission for each cycle present in the inventory. If the inventory is invalid, return `"None"` as a string.
|
|
685
|
-
"""
|
|
686
|
-
KEY = _InventoryKey.SHARE_OF_EMISSION
|
|
687
|
-
|
|
688
|
-
unique_cycles = sorted(
|
|
689
|
-
set(non_empty_list(flatten(list(group[KEY]) for group in cycle_inventory.values()))),
|
|
690
|
-
key=lambda id: next((year, id) for year in cycle_inventory if id in cycle_inventory[year][KEY])
|
|
691
|
-
)
|
|
692
|
-
|
|
693
|
-
should_run = cycle_inventory and len(unique_cycles) > 0
|
|
694
|
-
|
|
695
|
-
return log_as_table(
|
|
696
|
-
{
|
|
697
|
-
"year": year,
|
|
698
|
-
**{
|
|
699
|
-
id: _format_number(group.get(KEY, {}).get(id, 0)) for id in unique_cycles
|
|
700
|
-
}
|
|
701
|
-
} for year, group in cycle_inventory.items()
|
|
702
|
-
) if should_run else "None"
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
def _format_soc_inventory(soc_inventory: dict) -> str:
|
|
706
|
-
"""
|
|
707
|
-
Format the SOC inventory for logging as a table. Rows represent inventory years, columns represent soc stock change
|
|
708
|
-
data for each measurement method classification present in inventory. If the inventory is invalid, return `"None"`
|
|
709
|
-
as a string.
|
|
710
|
-
"""
|
|
711
|
-
KEYS = [
|
|
712
|
-
_InventoryKey.SOC_STOCK,
|
|
713
|
-
_InventoryKey.SOC_STOCK_CHANGE,
|
|
714
|
-
_InventoryKey.CO2_EMISSION
|
|
715
|
-
]
|
|
716
|
-
|
|
717
|
-
methods = soc_inventory.keys()
|
|
718
|
-
method_columns = list(product(methods, KEYS))
|
|
719
|
-
inventory_years = sorted(set(non_empty_list(flatten(list(years) for years in soc_inventory.values()))))
|
|
720
|
-
|
|
721
|
-
should_run = soc_inventory and len(inventory_years) > 0
|
|
722
|
-
|
|
723
|
-
return log_as_table(
|
|
724
|
-
{
|
|
725
|
-
"year": year,
|
|
726
|
-
**{
|
|
727
|
-
_format_column_header(method, key): _format_named_tuple(
|
|
728
|
-
soc_inventory.get(method, {}).get(year, {}).get(key, {})
|
|
729
|
-
) for method, key in method_columns
|
|
730
|
-
}
|
|
731
|
-
} for year in inventory_years
|
|
732
|
-
) if should_run else "None"
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
def _format_number(value: Optional[float]) -> str:
|
|
736
|
-
"""Format a float for logging in a table. If the value is invalid, return `"None"` as a string."""
|
|
737
|
-
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
def _format_column_header(method: MeasurementMethodClassification, inventory_key: _InventoryKey) -> str:
|
|
741
|
-
"""
|
|
742
|
-
Format a measurement method classification and inventory key for logging in a table as a column header. Replace any
|
|
743
|
-
whitespaces in the method value with dashes and concatenate it with the inventory key value, which already has the
|
|
744
|
-
correct format.
|
|
745
|
-
"""
|
|
746
|
-
return "-".join([
|
|
747
|
-
method.value.replace(" ", "-"),
|
|
748
|
-
inventory_key.value
|
|
247
|
+
len(sd) == 0 or len(sd) == len(value),
|
|
248
|
+
all(_get_datestr_format(datestr) in _VALID_DATE_FORMATS for datestr in dates)
|
|
749
249
|
])
|
|
750
250
|
|
|
751
251
|
|
|
752
|
-
def _format_named_tuple(value: Optional[Union[SocStock, SocStockChange, SocStockChangeEmission]]) -> str:
|
|
753
|
-
"""
|
|
754
|
-
Format a named tuple (`SocStock`, `SocStockChange` or `SocStockChangeEmission`) for logging in a table. Extract and
|
|
755
|
-
format just the value and discard the other data. If the value is invalid, return `"None"` as a string.
|
|
756
|
-
"""
|
|
757
|
-
return (
|
|
758
|
-
_format_number(value.value)
|
|
759
|
-
if isinstance(value, (SocStock, SocStockChange, SocStockChangeEmission))
|
|
760
|
-
else "None"
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
|
|
764
252
|
def _run(cycle_id: str, inventory: dict) -> list[dict]:
|
|
765
253
|
"""
|
|
766
254
|
Calculate emissions for a specific cycle using grouped SOC stock change and share of emissions data.
|
|
@@ -780,57 +268,21 @@ def _run(cycle_id: str, inventory: dict) -> list[dict]:
|
|
|
780
268
|
list[dict]
|
|
781
269
|
A list containing emission data calculated for the specified cycle.
|
|
782
270
|
"""
|
|
783
|
-
|
|
784
|
-
|
|
271
|
+
rescaled_emissions = [
|
|
272
|
+
rescale_carbon_stock_change_emission(
|
|
785
273
|
group[_InventoryKey.CO2_EMISSION], group[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
|
|
786
274
|
) for group in inventory.values()
|
|
787
|
-
]
|
|
788
|
-
|
|
789
|
-
value = round(total_emission.value, 6)
|
|
790
|
-
method_tier = total_emission.method
|
|
791
|
-
return [_emission(value, method_tier)]
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
def _rescale_stock_change_emission(emission: SocStockChangeEmission, factor: float) -> SocStockChangeEmission:
|
|
795
|
-
"""
|
|
796
|
-
Rescale an `SocStockChangeEmission` by a specified factor.
|
|
797
|
-
|
|
798
|
-
Parameters
|
|
799
|
-
----------
|
|
800
|
-
emission : SocStockChangeEmission
|
|
801
|
-
An SOC stock change emission (kg CO2 ha-1).
|
|
802
|
-
factor : float
|
|
803
|
-
A scaling factor (e.g., a [Cycles](https://www.hestia.earth/schema/Cycle)'s share of an annual emission).
|
|
804
|
-
|
|
805
|
-
Returns
|
|
806
|
-
-------
|
|
807
|
-
SocStockChangeEmission
|
|
808
|
-
The rescaled emission.
|
|
809
|
-
"""
|
|
810
|
-
value = emission.value * factor
|
|
811
|
-
return SocStockChangeEmission(value, emission.start_date, emission.end_date, emission.method)
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
def _sum_soc_stock_change_emissions(emissions: Iterable[SocStockChangeEmission]) -> SocStockChangeEmission:
|
|
815
|
-
"""
|
|
816
|
-
Sum together multiple `SocStockChangeEmission`s.
|
|
817
|
-
|
|
818
|
-
Parameters
|
|
819
|
-
----------
|
|
820
|
-
emissions : Iterable[SocStockChangeEmission]
|
|
821
|
-
A list of SOC stock change emissions (kg CO2 ha-1).
|
|
275
|
+
]
|
|
276
|
+
total_emission = reduce(add_carbon_stock_change_emissions, rescaled_emissions)
|
|
822
277
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
value = sum(e.value for e in emissions)
|
|
829
|
-
start_date = min(e.start_date for e in emissions)
|
|
830
|
-
end_date = max(e.end_date for e in emissions)
|
|
831
|
-
method = min_emission_method_tier(e.method for e in emissions)
|
|
278
|
+
descriptive_stats = calc_descriptive_stats(
|
|
279
|
+
total_emission.value,
|
|
280
|
+
EmissionStatsDefinition.SIMULATED,
|
|
281
|
+
decimals=6
|
|
282
|
+
)
|
|
832
283
|
|
|
833
|
-
|
|
284
|
+
method_tier = total_emission.method
|
|
285
|
+
return [_emission(method_tier=method_tier, **descriptive_stats)]
|
|
834
286
|
|
|
835
287
|
|
|
836
288
|
def run(cycle: dict) -> list[dict]:
|