hestia-earth-models 0.61.6__py3-none-any.whl → 0.61.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/completeness/electricityFuel.py +56 -0
- hestia_earth/models/cycle/input/hestiaAggregatedData.py +1 -1
- hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +44 -59
- hestia_earth/models/geospatialDatabase/histosol.py +4 -0
- hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +4 -2
- hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +1 -1
- hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +1 -1
- hestia_earth/models/ipcc2019/belowGroundCropResidue.py +1 -1
- hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -1
- hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +511 -458
- hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +5 -1
- hestia_earth/models/ipcc2019/organicCarbonPerHa.py +117 -3881
- hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2060 -0
- hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1630 -0
- hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +324 -0
- hestia_earth/models/mocking/search-results.json +360 -260
- hestia_earth/models/schererPfister2015/pToDrainageWaterSoilFlux.py +1 -1
- hestia_earth/models/schererPfister2015/pToGroundwaterSoilFlux.py +1 -1
- hestia_earth/models/site/organicCarbonPerHa.py +58 -44
- hestia_earth/models/site/soilMeasurement.py +25 -38
- hestia_earth/models/utils/__init__.py +28 -0
- hestia_earth/models/utils/aquacultureManagement.py +2 -2
- hestia_earth/models/utils/array_builders.py +578 -0
- hestia_earth/models/utils/blank_node.py +2 -3
- hestia_earth/models/utils/crop.py +24 -1
- hestia_earth/models/utils/cycle.py +0 -23
- hestia_earth/models/utils/descriptive_stats.py +285 -0
- hestia_earth/models/utils/emission.py +73 -2
- hestia_earth/models/utils/inorganicFertiliser.py +2 -2
- hestia_earth/models/utils/lookup.py +6 -3
- hestia_earth/models/utils/measurement.py +118 -4
- hestia_earth/models/utils/site.py +25 -13
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/METADATA +1 -1
- {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/RECORD +52 -40
- tests/models/cycle/completeness/test_electricityFuel.py +21 -0
- tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +2 -2
- tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +54 -165
- tests/models/ipcc2019/test_organicCarbonPerHa.py +219 -460
- tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +471 -0
- tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +208 -0
- tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +75 -0
- tests/models/site/test_organicCarbonPerHa.py +3 -12
- tests/models/site/test_soilMeasurement.py +5 -19
- tests/models/utils/test_array_builders.py +253 -0
- tests/models/utils/{test_cycle.py → test_crop.py} +2 -2
- tests/models/utils/test_descriptive_stats.py +134 -0
- tests/models/utils/test_emission.py +51 -1
- tests/models/utils/test_measurement.py +54 -2
- {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from functools import reduce
|
|
5
|
+
from itertools import product
|
|
4
6
|
from pydash.objects import merge
|
|
5
7
|
from typing import NamedTuple, Optional, Union
|
|
6
8
|
|
|
@@ -8,15 +10,19 @@ from hestia_earth.schema import (
|
|
|
8
10
|
CycleFunctionalUnit, EmissionMethodTier, MeasurementMethodClassification
|
|
9
11
|
)
|
|
10
12
|
from hestia_earth.utils.date import diff_in_days
|
|
11
|
-
from hestia_earth.utils.tools import flatten, non_empty_list
|
|
13
|
+
from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
|
|
12
14
|
|
|
13
15
|
from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
|
|
16
|
+
from hestia_earth.models.utils import pairwise
|
|
14
17
|
from hestia_earth.models.utils.blank_node import (
|
|
15
|
-
|
|
18
|
+
_get_datestr_format, _gapfill_datestr, DatestrGapfillMode, DatestrFormat, group_nodes_by_year, node_term_match
|
|
16
19
|
)
|
|
17
20
|
from hestia_earth.models.utils.constant import Units, get_atomic_conversion
|
|
18
|
-
from hestia_earth.models.utils.emission import _new_emission
|
|
19
|
-
from hestia_earth.models.utils.measurement import
|
|
21
|
+
from hestia_earth.models.utils.emission import _new_emission, min_emission_method_tier
|
|
22
|
+
from hestia_earth.models.utils.measurement import (
|
|
23
|
+
group_measurements_by_method_classification, min_measurement_method_classification,
|
|
24
|
+
to_measurement_method_classification
|
|
25
|
+
)
|
|
20
26
|
from hestia_earth.models.utils.site import related_cycles
|
|
21
27
|
|
|
22
28
|
from .utils import check_consecutive
|
|
@@ -57,580 +63,679 @@ DEPTH_LOWER = 30
|
|
|
57
63
|
|
|
58
64
|
ORGANIC_CARBON_PER_HA_TERM_ID = 'organicCarbonPerHa'
|
|
59
65
|
|
|
66
|
+
VALID_DATE_FORMATS = {
|
|
67
|
+
DatestrFormat.YEAR,
|
|
68
|
+
DatestrFormat.YEAR_MONTH,
|
|
69
|
+
DatestrFormat.YEAR_MONTH_DAY,
|
|
70
|
+
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
VALID_MEASUREMENT_METHOD_CLASSIFICATIONS = [
|
|
74
|
+
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT,
|
|
75
|
+
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
|
|
76
|
+
MeasurementMethodClassification.TIER_3_MODEL,
|
|
77
|
+
MeasurementMethodClassification.TIER_2_MODEL,
|
|
78
|
+
MeasurementMethodClassification.TIER_1_MODEL
|
|
79
|
+
]
|
|
80
|
+
"""
|
|
81
|
+
The list of `MeasurementMethodClassification`s that can be used to calculate SOC stock change emissions, ranked in
|
|
82
|
+
order from strongest to weakest.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _InventoryKey(Enum):
|
|
87
|
+
"""
|
|
88
|
+
The inner keys of the annualised inventory created by the `_compile_inventory` function.
|
|
89
|
+
|
|
90
|
+
The value of each enum member is formatted to be used as a column header in the `log_as_table` function.
|
|
91
|
+
"""
|
|
92
|
+
SOC_STOCK = "soc-stock"
|
|
93
|
+
SOC_STOCK_CHANGE = "soc-stock-change"
|
|
94
|
+
CO2_EMISSION = "co2-emission"
|
|
95
|
+
SHARE_OF_EMISSION = "share-of-emissions"
|
|
96
|
+
|
|
60
97
|
|
|
61
98
|
SocStock = NamedTuple("SocStock", [
|
|
62
99
|
("value", float),
|
|
100
|
+
("date", str),
|
|
63
101
|
("method", MeasurementMethodClassification)
|
|
64
102
|
])
|
|
65
103
|
"""
|
|
66
|
-
NamedTuple representing
|
|
104
|
+
NamedTuple representing an SOC stock.
|
|
67
105
|
|
|
68
106
|
Attributes
|
|
69
107
|
----------
|
|
70
108
|
value : float
|
|
71
109
|
The value of the SOC stock (kg C ha-1).
|
|
110
|
+
date : str
|
|
111
|
+
The date of the measurement as a datestr with format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
112
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
72
113
|
method: MeasurementMethodClassification
|
|
73
114
|
The measurement method for the SOC stock.
|
|
74
115
|
"""
|
|
75
116
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
117
|
+
SocStockChange = NamedTuple("SocStockChange", [
|
|
118
|
+
("value", float),
|
|
119
|
+
("start_date", str),
|
|
120
|
+
("end_date", str),
|
|
121
|
+
("method", MeasurementMethodClassification)
|
|
81
122
|
])
|
|
82
123
|
"""
|
|
83
|
-
|
|
84
|
-
"""
|
|
85
|
-
|
|
124
|
+
NamedTuple representing an SOC stock change.
|
|
86
125
|
|
|
87
|
-
|
|
126
|
+
Attributes
|
|
127
|
+
----------
|
|
128
|
+
value : float
|
|
129
|
+
The value of the SOC stock change (kg C ha-1).
|
|
130
|
+
start_date : str
|
|
131
|
+
The start date of the SOC stock change event as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
132
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
133
|
+
end_date : str
|
|
134
|
+
The end date of the SOC stock change event as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
135
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
136
|
+
method: MeasurementMethodClassification
|
|
137
|
+
The measurement method for the SOC stock change.
|
|
138
|
+
"""
|
|
88
139
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
MeasurementMethodClassification.PHYSICAL_MEASUREMENT_ON_NEARBY_SITE,
|
|
96
|
-
MeasurementMethodClassification.TIER_1_MODEL,
|
|
97
|
-
MeasurementMethodClassification.TIER_2_MODEL,
|
|
98
|
-
MeasurementMethodClassification.TIER_3_MODEL,
|
|
99
|
-
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS,
|
|
100
|
-
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT
|
|
101
|
-
]
|
|
140
|
+
SocStockChangeEmission = NamedTuple("SocStockChangeEmission", [
|
|
141
|
+
("value", float),
|
|
142
|
+
("start_date", str),
|
|
143
|
+
("end_date", str),
|
|
144
|
+
("method", EmissionMethodTier)
|
|
145
|
+
])
|
|
102
146
|
"""
|
|
103
|
-
|
|
104
|
-
the `co2ToAirSoilOrganicCarbonStockChangeManagementChange` output.
|
|
147
|
+
NamedTuple representing an SOC stock change emission.
|
|
105
148
|
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
Attributes
|
|
150
|
+
----------
|
|
151
|
+
value : float
|
|
152
|
+
The value of the SOC stock change (kg CO2 ha-1).
|
|
153
|
+
start_date : str
|
|
154
|
+
The start date of the SOC stock change emission as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
155
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
156
|
+
end_date : str
|
|
157
|
+
The end date of the SOC stock change emission as a datestr with the format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
158
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
159
|
+
method: MeasurementMethodClassification
|
|
160
|
+
The emission method tier.
|
|
108
161
|
"""
|
|
109
162
|
|
|
110
163
|
|
|
111
|
-
def
|
|
112
|
-
method: Union[str, MeasurementMethodClassification]
|
|
113
|
-
) -> Optional[MeasurementMethodClassification]:
|
|
164
|
+
def _emission(value: float, method_tier: EmissionMethodTier) -> dict:
|
|
114
165
|
"""
|
|
115
|
-
|
|
166
|
+
Create an emission node based on the provided value and method tier.
|
|
167
|
+
|
|
168
|
+
See [Emission schema](https://www.hestia.earth/schema/Emission) for more information.
|
|
116
169
|
|
|
117
170
|
Parameters
|
|
118
171
|
----------
|
|
119
|
-
|
|
120
|
-
The
|
|
172
|
+
value : float
|
|
173
|
+
The emission value (kg CO2 ha-1).
|
|
174
|
+
|
|
175
|
+
method_tier : EmissionMethodTier
|
|
176
|
+
The emission method tier.
|
|
121
177
|
|
|
122
178
|
Returns
|
|
123
179
|
-------
|
|
124
|
-
|
|
125
|
-
The
|
|
180
|
+
dict
|
|
181
|
+
The emission dictionary with keys 'depth', 'value', and 'methodTier'.
|
|
126
182
|
"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
183
|
+
emission = _new_emission(TERM_ID, MODEL)
|
|
184
|
+
emission["depth"] = DEPTH_LOWER
|
|
185
|
+
emission["value"] = [value]
|
|
186
|
+
emission["methodTier"] = method_tier.value
|
|
187
|
+
return emission
|
|
132
188
|
|
|
133
189
|
|
|
134
|
-
def
|
|
135
|
-
*methods: Union[MeasurementMethodClassification, Iterable[MeasurementMethodClassification]]
|
|
136
|
-
) -> MeasurementMethodClassification:
|
|
190
|
+
def _should_run(cycle: dict) -> tuple[bool, str, dict]:
|
|
137
191
|
"""
|
|
138
|
-
|
|
192
|
+
Determine if calculations should run for a given [Cycle](https://www.hestia.earth/schema/Cycle) based on SOC stock
|
|
193
|
+
and emissions data.
|
|
139
194
|
|
|
140
195
|
Parameters
|
|
141
196
|
----------
|
|
142
|
-
|
|
143
|
-
|
|
197
|
+
cycle : dict
|
|
198
|
+
The cycle dictionary for which the calculations will be evaluated.
|
|
144
199
|
|
|
145
200
|
Returns
|
|
146
201
|
-------
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
tuple[bool, str, dict]
|
|
203
|
+
`(should_run, cycle_id, inventory)`
|
|
149
204
|
"""
|
|
205
|
+
cycle_id = cycle.get("@id")
|
|
206
|
+
site = _get_site(cycle)
|
|
207
|
+
soc_measurements = [node for node in site.get("measurements", []) if _validate_soc_measurement(node)]
|
|
208
|
+
cycles = related_cycles(site)
|
|
150
209
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
[_to_measurement_method_classification(method) for method in arg] if isinstance(arg, Iterable)
|
|
155
|
-
else [_to_measurement_method_classification(arg)] for arg in methods
|
|
156
|
-
]))
|
|
157
|
-
|
|
158
|
-
return min(
|
|
159
|
-
_methods,
|
|
160
|
-
key=lambda method: MEASUREMENT_METHOD_RANKING.index(method),
|
|
161
|
-
default=list(MEASUREMENT_METHOD_RANKING)[0]
|
|
162
|
-
)
|
|
210
|
+
has_soc_measurements = len(soc_measurements) > 0
|
|
211
|
+
has_cycles = len(cycles) > 0
|
|
212
|
+
has_functional_unit_1_ha = all(cycle.get('functionalUnit') == CycleFunctionalUnit._1_HA.value for cycle in cycles)
|
|
163
213
|
|
|
214
|
+
should_compile_inventory = all([has_cycles, has_functional_unit_1_ha, has_soc_measurements])
|
|
164
215
|
|
|
165
|
-
|
|
166
|
-
*methods: Union[MeasurementMethodClassification, Iterable[MeasurementMethodClassification]]
|
|
167
|
-
) -> MeasurementMethodClassification:
|
|
168
|
-
"""
|
|
169
|
-
Get the max ranking measurement method from the provided methods.
|
|
216
|
+
inventory, logs = _compile_inventory(cycle_id, cycles, soc_measurements) if should_compile_inventory else ({}, {})
|
|
170
217
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
*methods : MeasurementMethodClassification | Iterable[MeasurementMethodClassification]
|
|
174
|
-
Measurement methods or iterables of measurement methods.
|
|
175
|
-
|
|
176
|
-
Returns
|
|
177
|
-
-------
|
|
178
|
-
MeasurementMethodClassification
|
|
179
|
-
The measurement method with the maximum ranking.
|
|
180
|
-
"""
|
|
218
|
+
has_valid_inventory = len(inventory) > 0
|
|
219
|
+
has_consecutive_years = check_consecutive(inventory.keys())
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
_methods,
|
|
191
|
-
key=lambda method: MEASUREMENT_METHOD_RANKING.index(method),
|
|
192
|
-
default=MEASUREMENT_METHOD_RANKING[-1]
|
|
221
|
+
logRequirements(
|
|
222
|
+
cycle, model=MODEL, term=TERM_ID,
|
|
223
|
+
has_soc_measurements=has_soc_measurements,
|
|
224
|
+
has_cycles=has_cycles,
|
|
225
|
+
has_functional_unit_1_ha=has_functional_unit_1_ha,
|
|
226
|
+
has_valid_inventory=has_valid_inventory,
|
|
227
|
+
has_consecutive_years=has_consecutive_years,
|
|
228
|
+
**logs
|
|
193
229
|
)
|
|
194
230
|
|
|
231
|
+
should_run = all([has_valid_inventory, has_consecutive_years])
|
|
195
232
|
|
|
196
|
-
|
|
197
|
-
MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER = {
|
|
198
|
-
MeasurementMethodClassification.TIER_2_MODEL: EmissionMethodTier.TIER_2,
|
|
199
|
-
MeasurementMethodClassification.TIER_3_MODEL: EmissionMethodTier.TIER_3,
|
|
200
|
-
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS: EmissionMethodTier.MEASURED,
|
|
201
|
-
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT: EmissionMethodTier.MEASURED,
|
|
202
|
-
}
|
|
203
|
-
"""
|
|
204
|
-
A mapping between `MeasurementMethodClassification`s and `EmissionMethodTier`s. As SOC measurements can be
|
|
205
|
-
measured/estimated through a variety of methods, the emission model needs be able to assign an emission tier for each.
|
|
206
|
-
Any `MeasurementMethodClassification` not in the mapping should be assigned `DEFAULT_EMISSION_METHOD_TIER`.
|
|
207
|
-
"""
|
|
233
|
+
logShouldRun(cycle, MODEL, TERM_ID, should_run)
|
|
208
234
|
|
|
235
|
+
return should_run, cycle_id, inventory
|
|
209
236
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
) -> EmissionMethodTier:
|
|
237
|
+
|
|
238
|
+
def _get_site(cycle: dict) -> dict:
|
|
213
239
|
"""
|
|
214
|
-
Get the
|
|
240
|
+
Get the [Site](https://www.hestia.earth/schema/Site) data from a [Cycle](https://www.hestia.earth/schema/Cycle).
|
|
215
241
|
|
|
216
242
|
Parameters
|
|
217
243
|
----------
|
|
218
|
-
|
|
219
|
-
The measurement method classification.
|
|
244
|
+
cycle : dict
|
|
220
245
|
|
|
221
246
|
Returns
|
|
222
247
|
-------
|
|
223
|
-
|
|
224
|
-
The corresponding emission method tier.
|
|
248
|
+
str
|
|
225
249
|
"""
|
|
226
|
-
return
|
|
227
|
-
measurement_method, DEFAULT_EMISSION_METHOD_TIER
|
|
228
|
-
)
|
|
250
|
+
return cycle.get("site", {})
|
|
229
251
|
|
|
230
252
|
|
|
231
|
-
def
|
|
232
|
-
value: float, method_tier: EmissionMethodTier
|
|
233
|
-
) -> dict:
|
|
253
|
+
def _validate_soc_measurement(node: dict) -> bool:
|
|
234
254
|
"""
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
See [Emission schema](https://www.hestia.earth/schema/Emission) for more information.
|
|
255
|
+
Validate a [Measurement](https://www.hestia.earth/schema/Measurement) to determine whether it is a valid
|
|
256
|
+
`organicCarbonPerHa` node.
|
|
238
257
|
|
|
239
258
|
Parameters
|
|
240
259
|
----------
|
|
241
|
-
|
|
242
|
-
The
|
|
243
|
-
|
|
244
|
-
method_tier : EmissionMethodTier
|
|
245
|
-
The emission method tier.
|
|
260
|
+
node : dict
|
|
261
|
+
The node to be validated.
|
|
246
262
|
|
|
247
263
|
Returns
|
|
248
264
|
-------
|
|
249
|
-
|
|
250
|
-
|
|
265
|
+
bool
|
|
266
|
+
`True` if the node passes all validation criteria, `False` otherwise.
|
|
251
267
|
"""
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
268
|
+
value = node.get("value", [])
|
|
269
|
+
dates = node.get("dates", [])
|
|
270
|
+
return all([
|
|
271
|
+
node_term_match(node, ORGANIC_CARBON_PER_HA_TERM_ID),
|
|
272
|
+
node.get("depthUpper") == DEPTH_UPPER,
|
|
273
|
+
node.get("depthLower") == DEPTH_LOWER,
|
|
274
|
+
node.get("methodClassification") in (m.value for m in VALID_MEASUREMENT_METHOD_CLASSIFICATIONS),
|
|
275
|
+
len(value) > 0,
|
|
276
|
+
len(value) == len(dates),
|
|
277
|
+
all(_get_datestr_format(datestr) in VALID_DATE_FORMATS for datestr in dates)
|
|
278
|
+
])
|
|
257
279
|
|
|
258
280
|
|
|
259
|
-
def
|
|
260
|
-
start_year: int,
|
|
261
|
-
end_year: int,
|
|
262
|
-
start_soc_stock: SocStock,
|
|
263
|
-
end_soc_stock: SocStock,
|
|
264
|
-
year: int
|
|
265
|
-
) -> SocStock:
|
|
281
|
+
def _compile_inventory(cycle_id: str, cycles: list[dict], soc_measurements: list[dict]) -> tuple[dict, dict]:
|
|
266
282
|
"""
|
|
267
|
-
|
|
283
|
+
Compile an annual inventory of SOC stocks, SOC stock changes, SOC stock change emissions and the share of emissions
|
|
284
|
+
of cycles.
|
|
268
285
|
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
A separate inventory is compiled for each valid `MeasurementMethodClassification` present in the input data, which
|
|
287
|
+
are then merged together by selecting the strongest available method for each relevant inventory year.
|
|
288
|
+
|
|
289
|
+
The returned inventory has the shape:
|
|
290
|
+
```
|
|
291
|
+
{
|
|
292
|
+
year (int): {
|
|
293
|
+
_InventoryKey.SOC_STOCK: value (SocStock),
|
|
294
|
+
_InventoryKey.SOC_STOCK_CHANGE: value (SocStockChange),
|
|
295
|
+
_InventoryKey.CO2_EMISSION: value (SocStockChangeEmission),
|
|
296
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
297
|
+
cycle_id (str): value (float),
|
|
298
|
+
...cycle_ids
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
...years
|
|
302
|
+
}
|
|
303
|
+
```
|
|
271
304
|
|
|
272
305
|
Parameters
|
|
273
306
|
----------
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
The end year for interpolation.
|
|
278
|
-
start_soc_stock : SocStock
|
|
279
|
-
The `SocStock` corresponding to the start year.
|
|
280
|
-
end_soc_stock : SocStock
|
|
281
|
-
The `SocStock` corresponding to the end year.
|
|
282
|
-
year : int
|
|
283
|
-
The target year for interpolation.
|
|
307
|
+
cycle_id : str
|
|
308
|
+
cycles : list[dict]
|
|
309
|
+
soc_measurements: list[dict]
|
|
284
310
|
|
|
285
311
|
Returns
|
|
286
312
|
-------
|
|
287
|
-
|
|
288
|
-
|
|
313
|
+
tuple[dict, dict]
|
|
314
|
+
`(inventory, logs)`
|
|
289
315
|
"""
|
|
290
|
-
|
|
316
|
+
cycle_inventory = _compile_cycle_inventory(cycles)
|
|
291
317
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
318
|
+
soc_measurements_by_method = group_measurements_by_method_classification(soc_measurements)
|
|
319
|
+
soc_inventory = {
|
|
320
|
+
method: _compile_soc_inventory(soc_measurements)
|
|
321
|
+
for method, soc_measurements in soc_measurements_by_method.items()
|
|
322
|
+
}
|
|
295
323
|
|
|
296
|
-
|
|
324
|
+
logs = {
|
|
325
|
+
"cycle_inventory": _format_cycle_inventory(cycle_inventory),
|
|
326
|
+
"soc_inventory": _format_soc_inventory(soc_inventory)
|
|
327
|
+
}
|
|
297
328
|
|
|
329
|
+
inventory = _squash_inventory(cycle_id, cycle_inventory, soc_inventory)
|
|
330
|
+
return inventory, logs
|
|
298
331
|
|
|
299
|
-
|
|
332
|
+
|
|
333
|
+
def _compile_cycle_inventory(cycles: list[dict]) -> dict:
|
|
300
334
|
"""
|
|
301
|
-
Calculate
|
|
335
|
+
Calculate grouped share of emissions for cycles based on the amount they contribute the the overall land management
|
|
336
|
+
of an inventory year.
|
|
302
337
|
|
|
303
|
-
|
|
338
|
+
This function groups cycles by year, then calculates the share of emissions for each cycle based on the
|
|
339
|
+
"fraction_of_group_duration" value. The share of emissions is normalized by the sum of cycle occupancies for the
|
|
340
|
+
entire dataset to ensure the values represent a valid share.
|
|
341
|
+
|
|
342
|
+
The returned inventory has the shape:
|
|
343
|
+
```
|
|
344
|
+
{
|
|
345
|
+
year (int): {
|
|
346
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
347
|
+
cycle_id (str): value (float),
|
|
348
|
+
...cycle_ids
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
...years
|
|
352
|
+
}
|
|
353
|
+
```
|
|
304
354
|
|
|
305
355
|
Parameters
|
|
306
356
|
----------
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
end_soc_stock : SocStock
|
|
311
|
-
The SOC stock at the end (kg C ha-1).
|
|
357
|
+
cycles : list[dict]
|
|
358
|
+
List of [Cycle nodes](https://www.hestia.earth/schema/Cycle), where each cycle dictionary should contain a
|
|
359
|
+
"fraction_of_group_duration" key added by the `group_nodes_by_year` function.
|
|
312
360
|
|
|
313
361
|
Returns
|
|
314
362
|
-------
|
|
315
|
-
|
|
316
|
-
|
|
363
|
+
dict
|
|
364
|
+
A dictionary with grouped share of emissions for each cycle based on the fraction of the year.
|
|
317
365
|
"""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
366
|
+
grouped_cycles = group_nodes_by_year(cycles)
|
|
367
|
+
return {
|
|
368
|
+
year: {
|
|
369
|
+
_InventoryKey.SHARE_OF_EMISSION: {
|
|
370
|
+
cycle["@id"]: (
|
|
371
|
+
cycle.get("fraction_of_group_duration", 0)
|
|
372
|
+
/ sum(cycle.get("fraction_of_group_duration", 0) for cycle in cycles)
|
|
373
|
+
) for cycle in cycles
|
|
374
|
+
}
|
|
375
|
+
} for year, cycles in grouped_cycles.items()
|
|
376
|
+
}
|
|
322
377
|
|
|
323
378
|
|
|
324
|
-
def
|
|
379
|
+
def _compile_soc_inventory(soc_measurements: list[dict]) -> dict:
|
|
325
380
|
"""
|
|
326
|
-
|
|
381
|
+
Compile an annual inventory of SOC stock data and pre-computed SOC stock change emissions.
|
|
327
382
|
|
|
328
|
-
|
|
383
|
+
The returned inventory has the shape:
|
|
384
|
+
```
|
|
385
|
+
{
|
|
386
|
+
year (int): {
|
|
387
|
+
_InventoryKey.SOC_STOCK: value (SocStock),
|
|
388
|
+
_InventoryKey.SOC_STOCK_CHANGE: value (SocStockChange),
|
|
389
|
+
_InventoryKey.CO2_EMISSION: value (SocStockChangeEmission)
|
|
390
|
+
},
|
|
391
|
+
...years
|
|
392
|
+
}
|
|
393
|
+
```
|
|
329
394
|
|
|
330
395
|
Parameters
|
|
331
396
|
----------
|
|
332
|
-
|
|
333
|
-
|
|
397
|
+
soc_measurements : list[dict]
|
|
398
|
+
List of pre-validated `organicCarbonPerHa` [Measurement nodes](https://www.hestia.earth/schema/Measurement).
|
|
334
399
|
|
|
335
400
|
Returns
|
|
336
401
|
-------
|
|
337
|
-
|
|
338
|
-
|
|
402
|
+
dict
|
|
403
|
+
The annual inventory.
|
|
339
404
|
"""
|
|
340
|
-
return kg_c * get_atomic_conversion(Units.KG_CO2, Units.TO_C)
|
|
341
405
|
|
|
406
|
+
values = flatten(measurement.get("value", []) for measurement in soc_measurements)
|
|
407
|
+
dates = flatten(
|
|
408
|
+
[_gapfill_datestr(datestr, DatestrGapfillMode.END) for datestr in measurement.get("dates", [])]
|
|
409
|
+
for measurement in soc_measurements
|
|
410
|
+
)
|
|
411
|
+
methods = flatten(
|
|
412
|
+
[MeasurementMethodClassification(measurement.get("methodClassification")) for _ in measurement.get("value", [])]
|
|
413
|
+
for measurement in soc_measurements
|
|
414
|
+
)
|
|
342
415
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
)
|
|
347
|
-
"""
|
|
348
|
-
Convert SOC stock change to CO2 emission using the given share of emission.
|
|
416
|
+
soc_stocks = sorted(
|
|
417
|
+
[SocStock(value, datestr, method) for value, datestr, method in zip(values, dates, methods)],
|
|
418
|
+
key=lambda soc_stock: soc_stock.date
|
|
419
|
+
)
|
|
349
420
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
soc_stock_change_value : float
|
|
353
|
-
The change in SOC stock value.
|
|
421
|
+
def interpolate_between(result: dict, soc_stock_pair: tuple[SocStock, SocStock]) -> dict:
|
|
422
|
+
start, end = soc_stock_pair[0], soc_stock_pair[1]
|
|
354
423
|
|
|
355
|
-
|
|
356
|
-
|
|
424
|
+
start_date = safe_parse_date(start.date, datetime.min)
|
|
425
|
+
end_date = safe_parse_date(end.date, datetime.min)
|
|
357
426
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"""
|
|
363
|
-
return -1 * share_of_emission * _convert_c_to_co2(soc_stock_change_value)
|
|
427
|
+
should_run = (
|
|
428
|
+
datetime.min != start_date != end_date
|
|
429
|
+
and end_date > start_date
|
|
430
|
+
)
|
|
364
431
|
|
|
432
|
+
update = {
|
|
433
|
+
year: {_InventoryKey.SOC_STOCK: lerp_soc_stocks(start, end, f"{year}-12-31T23:59:59")}
|
|
434
|
+
for year in range(start_date.year, end_date.year+1)
|
|
435
|
+
} if should_run else {}
|
|
365
436
|
|
|
366
|
-
|
|
437
|
+
return result | update
|
|
438
|
+
|
|
439
|
+
soc_stocks_by_year = reduce(interpolate_between, pairwise(soc_stocks), dict())
|
|
440
|
+
|
|
441
|
+
soc_stock_changes_by_year = {
|
|
442
|
+
year: {
|
|
443
|
+
_InventoryKey.SOC_STOCK_CHANGE: calc_soc_stock_change(
|
|
444
|
+
start_group[_InventoryKey.SOC_STOCK],
|
|
445
|
+
end_group[_InventoryKey.SOC_STOCK]
|
|
446
|
+
)
|
|
447
|
+
} for (_, start_group), (year, end_group) in pairwise(soc_stocks_by_year.items())
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
co2_emissions_by_method_and_year = {
|
|
451
|
+
year: {
|
|
452
|
+
_InventoryKey.CO2_EMISSION: calc_soc_stock_change_emission(
|
|
453
|
+
group[_InventoryKey.SOC_STOCK_CHANGE]
|
|
454
|
+
)
|
|
455
|
+
} for year, group in soc_stock_changes_by_year.items()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return _sorted_merge(soc_stocks_by_year, soc_stock_changes_by_year, co2_emissions_by_method_and_year)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def lerp_soc_stocks(start: SocStock, end: SocStock, target_date: str) -> SocStock:
|
|
367
462
|
"""
|
|
368
|
-
|
|
369
|
-
|
|
463
|
+
Estimate, using linear interpolation, an SOC stock for a specific date based on the the SOC stocks of two other
|
|
464
|
+
dates.
|
|
370
465
|
|
|
371
466
|
Parameters
|
|
372
467
|
----------
|
|
373
|
-
|
|
374
|
-
|
|
468
|
+
start : SocStock
|
|
469
|
+
The `SocStock` at the start (kg C ha-1).
|
|
470
|
+
end : SocStock
|
|
471
|
+
The `SocStock` at the end (kg C ha-1).
|
|
472
|
+
target_date : str
|
|
473
|
+
The target date for interpolation as a datestr with format `YYYY`, `YYYY-MM`, `YYYY-MM-DD` or
|
|
474
|
+
`YYYY-MM-DDTHH:mm:ss`.
|
|
375
475
|
|
|
376
476
|
Returns
|
|
377
477
|
-------
|
|
378
|
-
|
|
379
|
-
|
|
478
|
+
SocStock
|
|
479
|
+
The interpolated `SocStock` for the specified date (kg C ha-1).
|
|
380
480
|
"""
|
|
481
|
+
time_ratio = diff_in_days(start.date, target_date) / diff_in_days(start.date, end.date)
|
|
482
|
+
soc_delta = (end.value - start.value) * time_ratio
|
|
381
483
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
484
|
+
value = start.value + soc_delta
|
|
485
|
+
method = min_measurement_method_classification(start.method, end.method)
|
|
385
486
|
|
|
386
|
-
|
|
387
|
-
return dict(sorted(merged.items()))
|
|
487
|
+
return SocStock(value, target_date, method)
|
|
388
488
|
|
|
389
489
|
|
|
390
|
-
def
|
|
490
|
+
def calc_soc_stock_change(start: SocStock, end: SocStock) -> SocStockChange:
|
|
391
491
|
"""
|
|
392
|
-
|
|
492
|
+
Calculate the change in SOC stock change between the current and previous states.
|
|
493
|
+
|
|
494
|
+
The method should be the weaker of the two `MeasurementMethodClassification`s.
|
|
393
495
|
|
|
394
496
|
Parameters
|
|
395
497
|
----------
|
|
396
|
-
|
|
397
|
-
The SOC
|
|
498
|
+
start : SocStock
|
|
499
|
+
The SOC stock at the start (kg C ha-1).
|
|
500
|
+
|
|
501
|
+
end : SocStock
|
|
502
|
+
The SOC stock at the end (kg C ha-1).
|
|
398
503
|
|
|
399
504
|
Returns
|
|
400
505
|
-------
|
|
401
|
-
|
|
402
|
-
|
|
506
|
+
SocStockChange
|
|
507
|
+
The SOC stock change (kg C ha-1).
|
|
403
508
|
"""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
node.get("depthLower") == DEPTH_LOWER
|
|
408
|
-
])
|
|
509
|
+
value = end.value - start.value
|
|
510
|
+
method = min_measurement_method_classification(start.method, end.method)
|
|
511
|
+
return SocStockChange(value, start.date, end.date, method)
|
|
409
512
|
|
|
410
513
|
|
|
411
|
-
def
|
|
514
|
+
def calc_soc_stock_change_emission(soc_stock_change: SocStockChange) -> SocStockChangeEmission:
|
|
412
515
|
"""
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
Any nodes with missing or invalid `dates` field will already have been filtered out at this point, so we can assume
|
|
416
|
-
`node.value` and `node.dates` will have equal number of elements. See test case `missing-measurement-dates`.
|
|
516
|
+
Convert an `SocStockChange` into an `SocStockChangeEmission`.
|
|
417
517
|
|
|
418
518
|
Parameters
|
|
419
519
|
----------
|
|
420
|
-
|
|
421
|
-
The
|
|
422
|
-
|
|
423
|
-
nodes : List[dict]
|
|
424
|
-
List of [Measurement nodes](https://www.hestia.earth/schema/Measurement) containing SOC data.
|
|
520
|
+
soc_stock_change : SocStockChange
|
|
521
|
+
The SOC stock at the start (kg C ha-1).
|
|
425
522
|
|
|
426
523
|
Returns
|
|
427
524
|
-------
|
|
428
|
-
|
|
429
|
-
The
|
|
525
|
+
SocStockChangeEmission
|
|
526
|
+
The SOC stock change emission (kg CO2 ha-1).
|
|
430
527
|
"""
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
dates = flatten([measurement.get("dates", []) for measurement in nodes])
|
|
435
|
-
methods = flatten([
|
|
436
|
-
[measurement.get("methodClassification") for _ in measurement.get("value", [])]
|
|
437
|
-
for measurement in nodes
|
|
438
|
-
])
|
|
439
|
-
|
|
440
|
-
closest_date = min(
|
|
441
|
-
dates,
|
|
442
|
-
key=lambda date: abs(diff_in_days(date if date else OLDEST_DATE, target_date)),
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
closest_method = _get_max_measurement_method(
|
|
446
|
-
method for method, date in zip(methods, dates) if date == closest_date
|
|
447
|
-
)
|
|
528
|
+
value = convert_c_to_co2(soc_stock_change.value) * -1
|
|
529
|
+
method = convert_mmc_to_emt(soc_stock_change.method)
|
|
530
|
+
return SocStockChangeEmission(value, soc_stock_change.start_date, soc_stock_change.end_date, method)
|
|
448
531
|
|
|
449
|
-
value = next(
|
|
450
|
-
(value for value, method in zip(values, methods) if method == closest_method.value), 0
|
|
451
|
-
)
|
|
452
532
|
|
|
453
|
-
|
|
533
|
+
_DEFAULT_EMISSION_METHOD_TIER = EmissionMethodTier.TIER_1
|
|
534
|
+
_MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER = {
|
|
535
|
+
MeasurementMethodClassification.TIER_2_MODEL: EmissionMethodTier.TIER_2,
|
|
536
|
+
MeasurementMethodClassification.TIER_3_MODEL: EmissionMethodTier.TIER_3,
|
|
537
|
+
MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS: EmissionMethodTier.MEASURED,
|
|
538
|
+
MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT: EmissionMethodTier.MEASURED,
|
|
539
|
+
}
|
|
540
|
+
"""
|
|
541
|
+
A mapping between `MeasurementMethodClassification`s and `EmissionMethodTier`s. As SOC measurements can be
|
|
542
|
+
measured/estimated through a variety of methods, the emission model needs be able to assign an emission tier for each.
|
|
543
|
+
Any `MeasurementMethodClassification` not in the mapping should be assigned `DEFAULT_EMISSION_METHOD_TIER`.
|
|
544
|
+
"""
|
|
454
545
|
|
|
455
546
|
|
|
456
|
-
def
|
|
547
|
+
def convert_mmc_to_emt(
|
|
548
|
+
measurement_method_classification: MeasurementMethodClassification
|
|
549
|
+
) -> EmissionMethodTier:
|
|
457
550
|
"""
|
|
458
|
-
|
|
459
|
-
single `SocStock` for each year.
|
|
551
|
+
Get the emission method tier based on the provided measurement method classification.
|
|
460
552
|
|
|
461
553
|
Parameters
|
|
462
554
|
----------
|
|
463
|
-
|
|
464
|
-
|
|
555
|
+
measurement_method : MeasurementMethodClassification
|
|
556
|
+
The measurement method classification.
|
|
465
557
|
|
|
466
558
|
Returns
|
|
467
559
|
-------
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
information under the inner key specified by _InnerKey.SOC_STOCK.
|
|
560
|
+
EmissionMethodTier
|
|
561
|
+
The corresponding emission method tier.
|
|
471
562
|
"""
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
(node for node in site.get("measurements", []) if _validate_soc_measurement_node(node)),
|
|
476
|
-
mode=GroupNodesByYearMode.DATES
|
|
563
|
+
return _MEASUREMENT_METHOD_CLASSIFICATION_TO_EMISSION_METHOD_TIER.get(
|
|
564
|
+
to_measurement_method_classification(measurement_method_classification),
|
|
565
|
+
_DEFAULT_EMISSION_METHOD_TIER
|
|
477
566
|
)
|
|
478
567
|
|
|
479
|
-
return {
|
|
480
|
-
year: {
|
|
481
|
-
INNER_KEY: (
|
|
482
|
-
_nodes_to_soc_stock(year, nodes)
|
|
483
|
-
)
|
|
484
|
-
} for year, nodes in grouped_soc_measurements.items()
|
|
485
|
-
}
|
|
486
|
-
|
|
487
568
|
|
|
488
|
-
def
|
|
569
|
+
def convert_c_to_co2(kg_c: float) -> float:
|
|
489
570
|
"""
|
|
490
|
-
|
|
571
|
+
Convert mass of carbon (C) to carbon dioxide (CO2) using the atomic conversion ratio.
|
|
491
572
|
|
|
492
|
-
|
|
493
|
-
existing data points. The result is a dictionary with SOC stock information for all years, including those without
|
|
494
|
-
initially available data.
|
|
573
|
+
n.b. `get_atomic_conversion` returns the ratio C:CO2 (~44/12).
|
|
495
574
|
|
|
496
575
|
Parameters
|
|
497
576
|
----------
|
|
498
|
-
|
|
499
|
-
|
|
577
|
+
kg_c : float
|
|
578
|
+
Mass of carbon (C) to be converted to carbon dioxide (CO2) (kg C).
|
|
500
579
|
|
|
501
580
|
Returns
|
|
502
581
|
-------
|
|
503
|
-
|
|
504
|
-
|
|
582
|
+
float
|
|
583
|
+
Mass of carbon dioxide (CO2) resulting from the conversion (kg CO2).
|
|
505
584
|
"""
|
|
506
|
-
|
|
507
|
-
INNER_KEY = _InnerKey.SOC_STOCK
|
|
508
|
-
|
|
509
|
-
def group_interpolate(data: dict, i: int):
|
|
510
|
-
current_year = list(grouped_soc_stocks.keys())[i]
|
|
511
|
-
prev_year = list(grouped_soc_stocks.keys())[i-1]
|
|
512
|
-
|
|
513
|
-
current_soc_stock = grouped_soc_stocks[current_year][INNER_KEY]
|
|
514
|
-
prev_soc_stock = grouped_soc_stocks[prev_year][INNER_KEY]
|
|
515
|
-
|
|
516
|
-
return data | {
|
|
517
|
-
inner_year: {
|
|
518
|
-
INNER_KEY: _linear_interpolate_soc_stock(
|
|
519
|
-
prev_year, current_year, prev_soc_stock, current_soc_stock, inner_year
|
|
520
|
-
)
|
|
521
|
-
} for inner_year in range(prev_year+1, current_year)
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return reduce(group_interpolate, range(1, len(grouped_soc_stocks)), dict())
|
|
585
|
+
return kg_c * get_atomic_conversion(Units.KG_CO2, Units.TO_C)
|
|
525
586
|
|
|
526
587
|
|
|
527
|
-
def
|
|
588
|
+
def _sorted_merge(*sources: Union[dict, list[dict]]) -> dict:
|
|
528
589
|
"""
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
This function combines grouping and interpolation of SOC stocks for a given site, providing a comprehensive
|
|
532
|
-
dictionary with SOC stock information for all years.
|
|
590
|
+
Merge dictionaries and return the result as a new dictionary with keys sorted in order to preserve the temporal
|
|
591
|
+
order of inventory years.
|
|
533
592
|
|
|
534
593
|
Parameters
|
|
535
594
|
----------
|
|
536
|
-
|
|
537
|
-
|
|
595
|
+
*sources : dict | list[dict]
|
|
596
|
+
One or more dictionaries or lists of dictionaries to be merged.
|
|
538
597
|
|
|
539
598
|
Returns
|
|
540
599
|
-------
|
|
541
600
|
dict
|
|
542
|
-
A dictionary
|
|
601
|
+
A new dictionary containing the merged key-value pairs, with keys sorted.
|
|
543
602
|
"""
|
|
544
|
-
grouped_soc_stocks = _group_soc_stocks(site)
|
|
545
|
-
grouped_interpolated_soc_stocks = _interpolate_grouped_soc_stocks(grouped_soc_stocks)
|
|
546
|
-
return _sorted_merge(grouped_soc_stocks, grouped_interpolated_soc_stocks)
|
|
547
603
|
|
|
604
|
+
_sources = non_empty_list(
|
|
605
|
+
flatten([arg if isinstance(arg, list) else [arg] for arg in sources])
|
|
606
|
+
)
|
|
548
607
|
|
|
549
|
-
|
|
608
|
+
merged = reduce(merge, _sources, {})
|
|
609
|
+
return dict(sorted(merged.items()))
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _squash_inventory(cycle_id: str, cycle_inventory: dict, soc_inventory: dict) -> dict:
|
|
550
613
|
"""
|
|
551
|
-
|
|
614
|
+
Merge the `cycle_inventory` and `soc_inventory` for each inventory year by selecting the strongest available
|
|
615
|
+
`MeasurementMethodClassification`. Years that do not overlap with the Cycle node that the emission model is running
|
|
616
|
+
on should be discarded as they are not relevant.
|
|
552
617
|
|
|
553
618
|
Parameters
|
|
554
619
|
----------
|
|
555
|
-
|
|
556
|
-
|
|
620
|
+
cycle_id : str
|
|
621
|
+
cycle_inventory : dict
|
|
622
|
+
soc_inventory: dict
|
|
557
623
|
|
|
558
624
|
Returns
|
|
559
625
|
-------
|
|
560
626
|
dict
|
|
561
|
-
|
|
627
|
+
The squashed inventory.
|
|
628
|
+
"""
|
|
629
|
+
inventory_years = sorted(set(non_empty_list(
|
|
630
|
+
flatten(list(years) for years in soc_inventory.values())
|
|
631
|
+
+ list(cycle_inventory.keys())
|
|
632
|
+
)))
|
|
633
|
+
|
|
634
|
+
def should_run_group(method: MeasurementMethodClassification, year: int) -> bool:
|
|
635
|
+
soc_stock_inventory_group = soc_inventory.get(method, {}).get(year, {})
|
|
636
|
+
share_of_emissions_group = cycle_inventory.get(year, {})
|
|
637
|
+
|
|
638
|
+
has_emission = _InventoryKey.CO2_EMISSION in soc_stock_inventory_group.keys()
|
|
639
|
+
is_relevant_for_cycle = cycle_id in share_of_emissions_group.get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
|
|
640
|
+
return all([has_emission, is_relevant_for_cycle])
|
|
641
|
+
|
|
642
|
+
def squash(result: dict, year: int) -> dict:
|
|
643
|
+
update_dict = next(
|
|
644
|
+
(
|
|
645
|
+
{year: reduce(merge, [soc_inventory[method][year], cycle_inventory[year]], dict())}
|
|
646
|
+
for method in VALID_MEASUREMENT_METHOD_CLASSIFICATIONS if should_run_group(method, year)
|
|
647
|
+
),
|
|
648
|
+
{}
|
|
649
|
+
)
|
|
650
|
+
return result | update_dict
|
|
651
|
+
|
|
652
|
+
return reduce(squash, inventory_years, dict())
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _format_cycle_inventory(cycle_inventory: dict) -> str:
|
|
656
|
+
"""
|
|
657
|
+
Format the cycle inventory for logging as a table. Rows represent inventory years, columns represent the share of
|
|
658
|
+
emission for each cycle present in the inventory. If the inventory is invalid, return `"None"` as a string.
|
|
562
659
|
"""
|
|
563
|
-
|
|
660
|
+
KEY = _InventoryKey.SHARE_OF_EMISSION
|
|
564
661
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
662
|
+
unique_cycles = sorted(
|
|
663
|
+
set(non_empty_list(flatten(list(group[KEY]) for group in cycle_inventory.values()))),
|
|
664
|
+
key=lambda id: next((year, id) for year in cycle_inventory if id in cycle_inventory[year][KEY])
|
|
665
|
+
)
|
|
568
666
|
|
|
569
|
-
|
|
570
|
-
prev_soc_stock = grouped_soc_stocks[prev_year][_InnerKey.SOC_STOCK]
|
|
667
|
+
should_run = cycle_inventory and len(unique_cycles) > 0
|
|
571
668
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
669
|
+
return log_as_table(
|
|
670
|
+
{
|
|
671
|
+
"year": year,
|
|
672
|
+
**{
|
|
673
|
+
id: _format_number(group.get(KEY, {}).get(id, 0)) for id in unique_cycles
|
|
575
674
|
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return reduce(group_changes, range(1, len(grouped_soc_stocks)), dict())
|
|
675
|
+
} for year, group in cycle_inventory.items()
|
|
676
|
+
) if should_run else "None"
|
|
579
677
|
|
|
580
678
|
|
|
581
|
-
def
|
|
679
|
+
def _format_soc_inventory(soc_inventory: dict) -> str:
|
|
582
680
|
"""
|
|
583
|
-
|
|
584
|
-
`
|
|
681
|
+
Format the SOC inventory for logging as a table. Rows represent inventory years, columns represent soc stock change
|
|
682
|
+
data for each measurement method classification present in inventory. If the inventory is invalid, return `"None"`
|
|
683
|
+
as a string.
|
|
684
|
+
"""
|
|
685
|
+
KEYS = [
|
|
686
|
+
_InventoryKey.SOC_STOCK,
|
|
687
|
+
_InventoryKey.SOC_STOCK_CHANGE,
|
|
688
|
+
_InventoryKey.CO2_EMISSION
|
|
689
|
+
]
|
|
585
690
|
|
|
586
|
-
|
|
691
|
+
methods = soc_inventory.keys()
|
|
692
|
+
method_columns = list(product(methods, KEYS))
|
|
693
|
+
inventory_years = sorted(set(non_empty_list(flatten(list(years) for years in soc_inventory.values()))))
|
|
587
694
|
|
|
588
|
-
|
|
589
|
-
----------
|
|
590
|
-
cycles : List[dict]
|
|
591
|
-
List of cycles, where each cycle dictionary should contain a "fraction_of_group_duration" key.
|
|
695
|
+
should_run = soc_inventory and len(inventory_years) > 0
|
|
592
696
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
697
|
+
return log_as_table(
|
|
698
|
+
{
|
|
699
|
+
"year": year,
|
|
700
|
+
**{
|
|
701
|
+
_format_column_header(method, key): _format_named_tuple(
|
|
702
|
+
soc_inventory.get(method, {}).get(year, {}).get(key, {})
|
|
703
|
+
) for method, key in method_columns
|
|
704
|
+
}
|
|
705
|
+
} for year in inventory_years
|
|
706
|
+
) if should_run else "None"
|
|
599
707
|
|
|
600
708
|
|
|
601
|
-
def
|
|
602
|
-
"""
|
|
603
|
-
|
|
604
|
-
of an inventory year.
|
|
709
|
+
def _format_number(value: Optional[float]) -> str:
|
|
710
|
+
"""Format a float for logging in a table. If the value is invalid, return `"None"` as a string."""
|
|
711
|
+
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
605
712
|
|
|
606
|
-
This function groups cycles by year, then calculates the share of emissions for each cycle based on the
|
|
607
|
-
"fraction_of_group_duration" value. The share of emissions is normalized by the sum of cycle occupancies for the
|
|
608
|
-
entire dataset to ensure the values represent a valid share.
|
|
609
713
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
714
|
+
def _format_column_header(method: MeasurementMethodClassification, inventory_key: _InventoryKey) -> str:
|
|
715
|
+
"""
|
|
716
|
+
Format a measurement method classification and inventory key for logging in a table as a column header. Replace any
|
|
717
|
+
whitespaces in the method value with dashes and concatenate it with the inventory key value, which already has the
|
|
718
|
+
correct format.
|
|
719
|
+
"""
|
|
720
|
+
return "-".join([
|
|
721
|
+
method.value.replace(" ", "-"),
|
|
722
|
+
inventory_key.value
|
|
723
|
+
])
|
|
615
724
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
dict
|
|
619
|
-
A dictionary with grouped share of emissions for each cycle based on the fraction of the year.
|
|
725
|
+
|
|
726
|
+
def _format_named_tuple(value: Optional[Union[SocStock, SocStockChange, SocStockChangeEmission]]) -> str:
|
|
620
727
|
"""
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
} for year, cycles in grouped_cycles.items()
|
|
630
|
-
}
|
|
728
|
+
Format a named tuple (`SocStock`, `SocStockChange` or `SocStockChangeEmission`) for logging in a table. Extract and
|
|
729
|
+
format just the value and discard the other data. If the value is invalid, return `"None"` as a string.
|
|
730
|
+
"""
|
|
731
|
+
return (
|
|
732
|
+
_format_number(value.value)
|
|
733
|
+
if isinstance(value, (SocStock, SocStockChange, SocStockChangeEmission))
|
|
734
|
+
else "None"
|
|
735
|
+
)
|
|
631
736
|
|
|
632
737
|
|
|
633
|
-
def _run(cycle_id: str,
|
|
738
|
+
def _run(cycle_id: str, inventory: dict) -> list[dict]:
|
|
634
739
|
"""
|
|
635
740
|
Calculate emissions for a specific cycle using grouped SOC stock change and share of emissions data.
|
|
636
741
|
|
|
@@ -641,7 +746,6 @@ def _run(cycle_id: str, grouped_data: dict) -> list[dict]:
|
|
|
641
746
|
----------
|
|
642
747
|
cycle_id : str
|
|
643
748
|
The "@id" field of the [Cycle node](https://www.hestia.earth/schema/Cycle).
|
|
644
|
-
|
|
645
749
|
grouped_data : dict
|
|
646
750
|
A dictionary containing grouped SOC stock change and share of emissions data.
|
|
647
751
|
|
|
@@ -650,110 +754,59 @@ def _run(cycle_id: str, grouped_data: dict) -> list[dict]:
|
|
|
650
754
|
list[dict]
|
|
651
755
|
A list containing emission data calculated for the specified cycle.
|
|
652
756
|
"""
|
|
757
|
+
total_emission = _sum_soc_stock_change_emissions([
|
|
758
|
+
_rescale_stock_change_emission(
|
|
759
|
+
group[_InventoryKey.CO2_EMISSION], group[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
|
|
760
|
+
) for group in inventory.values()
|
|
761
|
+
])
|
|
653
762
|
|
|
654
|
-
value =
|
|
655
|
-
|
|
656
|
-
group[_InnerKey.SOC_STOCK_CHANGE].value,
|
|
657
|
-
group[_InnerKey.SHARE_OF_EMISSIONS].get(cycle_id, 1)
|
|
658
|
-
) for group in grouped_data.values()
|
|
659
|
-
)
|
|
660
|
-
|
|
661
|
-
method_tier = _get_emission_method_tier(
|
|
662
|
-
_get_min_measurement_method(
|
|
663
|
-
group[_InnerKey.SOC_STOCK_CHANGE].method for group in grouped_data.values()
|
|
664
|
-
)
|
|
665
|
-
)
|
|
666
|
-
|
|
763
|
+
value = round(total_emission.value, 6)
|
|
764
|
+
method_tier = total_emission.method
|
|
667
765
|
return [_emission(value, method_tier)]
|
|
668
766
|
|
|
669
767
|
|
|
670
|
-
def
|
|
671
|
-
return cycle.get("site", {})
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
def _should_run(cycle: dict) -> tuple:
|
|
768
|
+
def _rescale_stock_change_emission(emission: SocStockChangeEmission, factor: float) -> SocStockChangeEmission:
|
|
675
769
|
"""
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
This function assesses whether calculations should run for a given cycle by checking the availability of SOC stock
|
|
679
|
-
data and cycle nodes. It retrieves SOC stock data and for the site linked to the cycle, calculates SOC stock
|
|
680
|
-
changes and share of emissions, and merges the data into a grouped format. The function checks for the presence of
|
|
681
|
-
necessary keys in the grouped data and ensures that the years are consecutive before determining if calculations
|
|
682
|
-
should run.
|
|
770
|
+
Rescale an `SocStockChangeEmission` by a specified factor.
|
|
683
771
|
|
|
684
772
|
Parameters
|
|
685
773
|
----------
|
|
686
|
-
|
|
687
|
-
|
|
774
|
+
emission : SocStockChangeEmission
|
|
775
|
+
An SOC stock change emission (kg CO2 ha-1).
|
|
776
|
+
factor : float
|
|
777
|
+
A scaling factor (e.g., a [Cycles](https://www.hestia.earth/schema/Cycle)'s share of an annual emission).
|
|
688
778
|
|
|
689
779
|
Returns
|
|
690
780
|
-------
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
the cycle identifier, and grouped SOC stock and emissions data.
|
|
781
|
+
SocStockChangeEmission
|
|
782
|
+
The rescaled emission.
|
|
694
783
|
"""
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
cycles = related_cycles(site)
|
|
784
|
+
value = emission.value * factor
|
|
785
|
+
return SocStockChangeEmission(value, emission.start_date, emission.end_date, emission.method)
|
|
698
786
|
|
|
699
|
-
grouped_soc_stocks = _get_grouped_soc_stocks(site)
|
|
700
|
-
grouped_soc_stock_changes = _calc_grouped_soc_stock_changes(grouped_soc_stocks)
|
|
701
|
-
grouped_share_of_emissions = _calc_grouped_share_of_emissions(cycles)
|
|
702
787
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return is_data_complete and is_relevant_for_cycle
|
|
707
|
-
|
|
708
|
-
inventory = _sorted_merge(grouped_soc_stocks, grouped_soc_stock_changes, grouped_share_of_emissions)
|
|
709
|
-
valid_years = [year for year, group in inventory.items() if _should_run_group(year, group)]
|
|
710
|
-
|
|
711
|
-
num_organic_carbon_per_ha_measurements = len(
|
|
712
|
-
[node for node in site.get('measurements', []) if _validate_soc_measurement_node(node)]
|
|
713
|
-
)
|
|
714
|
-
has_organic_carbon_per_ha_measurements = num_organic_carbon_per_ha_measurements > 1
|
|
715
|
-
has_functional_unit_1_ha = all(
|
|
716
|
-
cycle.get('functionalUnit') == CycleFunctionalUnit._1_HA.value for cycle in cycles
|
|
717
|
-
)
|
|
718
|
-
has_valid_inventory = len(valid_years) > 0 and check_consecutive(valid_years)
|
|
788
|
+
def _sum_soc_stock_change_emissions(emissions: Iterable[SocStockChangeEmission]) -> SocStockChangeEmission:
|
|
789
|
+
"""
|
|
790
|
+
Sum together multiple `SocStockChangeEmission`s.
|
|
719
791
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
has_valid_inventory=has_valid_inventory,
|
|
725
|
-
inventory=log_as_table(
|
|
726
|
-
{
|
|
727
|
-
"year": year,
|
|
728
|
-
"should-run": year in valid_years,
|
|
729
|
-
"soc-stock": (
|
|
730
|
-
group.get(_InnerKey.SOC_STOCK).value if group.get(_InnerKey.SOC_STOCK)
|
|
731
|
-
else None
|
|
732
|
-
),
|
|
733
|
-
"soc-stock-method": (
|
|
734
|
-
group.get(_InnerKey.SOC_STOCK).method.value if group.get(_InnerKey.SOC_STOCK)
|
|
735
|
-
else None
|
|
736
|
-
),
|
|
737
|
-
"soc-stock-change": (
|
|
738
|
-
group.get(_InnerKey.SOC_STOCK_CHANGE).value if group.get(_InnerKey.SOC_STOCK_CHANGE)
|
|
739
|
-
else None
|
|
740
|
-
),
|
|
741
|
-
"soc-stock-change-method": (
|
|
742
|
-
group.get(_InnerKey.SOC_STOCK_CHANGE).method.value if group.get(_InnerKey.SOC_STOCK_CHANGE)
|
|
743
|
-
else None
|
|
744
|
-
),
|
|
745
|
-
"share-of-emission": group.get(_InnerKey.SHARE_OF_EMISSIONS, {}).get(cycle_id, 0)
|
|
746
|
-
} for year, group in inventory.items()
|
|
747
|
-
)
|
|
748
|
-
)
|
|
792
|
+
Parameters
|
|
793
|
+
----------
|
|
794
|
+
emissions : Iterable[SocStockChangeEmission]
|
|
795
|
+
A list of SOC stock change emissions (kg CO2 ha-1).
|
|
749
796
|
|
|
750
|
-
|
|
751
|
-
|
|
797
|
+
Returns
|
|
798
|
+
-------
|
|
799
|
+
SocStockChangeEmission
|
|
800
|
+
The summed emission.
|
|
801
|
+
"""
|
|
802
|
+
value = sum(e.value for e in emissions)
|
|
803
|
+
start_date = min(e.start_date for e in emissions)
|
|
804
|
+
end_date = max(e.end_date for e in emissions)
|
|
805
|
+
method = min_emission_method_tier(e.method for e in emissions)
|
|
752
806
|
|
|
753
|
-
return
|
|
807
|
+
return SocStockChangeEmission(value, start_date, end_date, method)
|
|
754
808
|
|
|
755
809
|
|
|
756
810
|
def run(cycle: dict) -> list[dict]:
|
|
757
811
|
should_run, *args = _should_run(cycle)
|
|
758
|
-
|
|
759
812
|
return _run(*args) if should_run else []
|