hestia-earth-models 0.74.13__py3-none-any.whl → 0.74.15__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/config/ImpactAssessment.json +4 -2
- hestia_earth/models/config/Site.json +11 -3
- hestia_earth/models/ecoalimV9/cycle.py +20 -5
- hestia_earth/models/ecoalimV9/utils.py +52 -2
- hestia_earth/models/emepEea2019/fuelCombustion_utils.py +21 -21
- hestia_earth/models/environmentalFootprintV3_1/environmentalFootprintSingleOverallScore.py +27 -18
- hestia_earth/models/hestia/landCover.py +27 -41
- hestia_earth/models/hestia/landCover_utils.py +40 -45
- hestia_earth/models/hestia/landOccupationDuringCycle.py +9 -27
- hestia_earth/models/hestia/soilClassification.py +314 -0
- hestia_earth/models/ipcc2019/aboveGroundBiomass.py +5 -15
- hestia_earth/models/ipcc2019/belowGroundBiomass.py +5 -15
- hestia_earth/models/ipcc2019/biocharOrganicCarbonPerHa.py +5 -39
- hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +2 -1
- hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +5 -5
- hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +6 -21
- hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +4 -5
- hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +5 -5
- hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +17 -46
- hestia_earth/models/ipcc2019/organicCarbonPerHa.py +10 -10
- hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +4 -19
- hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +0 -9
- hestia_earth/models/log.py +71 -1
- hestia_earth/models/mocking/search-results.json +1 -1
- hestia_earth/models/utils/__init__.py +2 -2
- hestia_earth/models/utils/aggregated.py +3 -3
- hestia_earth/models/utils/blank_node.py +12 -4
- hestia_earth/models/utils/lookup.py +9 -10
- hestia_earth/models/version.py +1 -1
- {hestia_earth_models-0.74.13.dist-info → hestia_earth_models-0.74.15.dist-info}/METADATA +2 -2
- {hestia_earth_models-0.74.13.dist-info → hestia_earth_models-0.74.15.dist-info}/RECORD +39 -36
- tests/models/ecoalimV9/test_cycle.py +3 -2
- tests/models/hestia/test_landCover.py +1 -1
- tests/models/hestia/test_soilClassification.py +72 -0
- tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +4 -48
- tests/models/test_log.py +128 -0
- {hestia_earth_models-0.74.13.dist-info → hestia_earth_models-0.74.15.dist-info}/LICENSE +0 -0
- {hestia_earth_models-0.74.13.dist-info → hestia_earth_models-0.74.15.dist-info}/WHEEL +0 -0
- {hestia_earth_models-0.74.13.dist-info → hestia_earth_models-0.74.15.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from functools import reduce
|
|
2
|
+
from typing import NamedTuple, Optional
|
|
3
|
+
from pydash import merge
|
|
4
|
+
|
|
5
|
+
from hestia_earth.schema import MeasurementMethodClassification, TermTermType
|
|
6
|
+
from hestia_earth.utils.blank_node import get_node_value, flatten
|
|
7
|
+
from hestia_earth.utils.model import filter_list_term_type
|
|
8
|
+
|
|
9
|
+
from hestia_earth.models.hestia.soilMeasurement import STANDARD_DEPTHS
|
|
10
|
+
from hestia_earth.models.ipcc2019.organicCarbonPerHa_utils import (
|
|
11
|
+
IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE, IpccSoilCategory
|
|
12
|
+
)
|
|
13
|
+
from hestia_earth.models.log import format_bool, format_float, format_str, log_as_table, logRequirements, logShouldRun
|
|
14
|
+
from hestia_earth.models.utils import split_on_condition
|
|
15
|
+
from hestia_earth.models.utils.blank_node import node_lookup_match, split_nodes_by_dates
|
|
16
|
+
from hestia_earth.models.utils.measurement import _new_measurement
|
|
17
|
+
from . import MODEL
|
|
18
|
+
|
|
19
|
+
REQUIREMENTS = {
|
|
20
|
+
"Site": {
|
|
21
|
+
"optional": {
|
|
22
|
+
"measurements": [{
|
|
23
|
+
"@type": "Measurement",
|
|
24
|
+
"value": "",
|
|
25
|
+
"depthUpper": "",
|
|
26
|
+
"depthLower": "",
|
|
27
|
+
"term.termType": "soilType",
|
|
28
|
+
"optional": {
|
|
29
|
+
"dates": ""
|
|
30
|
+
}
|
|
31
|
+
}]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
RETURNS = {
|
|
36
|
+
"Measurement": [{
|
|
37
|
+
"value": "",
|
|
38
|
+
"depthUpper": "",
|
|
39
|
+
"depthLower": "",
|
|
40
|
+
"methodClassification": "modelled using other measurements"
|
|
41
|
+
}]
|
|
42
|
+
}
|
|
43
|
+
LOOKUPS = {
|
|
44
|
+
"soilType": "IPCC_SOIL_CATEGORY"
|
|
45
|
+
}
|
|
46
|
+
TERM_ID = 'organicSoils,mineralSoils'
|
|
47
|
+
|
|
48
|
+
MEASUREMENT_TERM_IDS = TERM_ID.split(',')
|
|
49
|
+
ORGANIC_SOILS_TERM_ID = MEASUREMENT_TERM_IDS[0]
|
|
50
|
+
MINERAL_SOILS_TERM_ID = MEASUREMENT_TERM_IDS[1]
|
|
51
|
+
METHOD = MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS.value
|
|
52
|
+
|
|
53
|
+
TARGET_LOOKUP_VALUE = IPCC_SOIL_CATEGORY_TO_SOIL_TYPE_LOOKUP_VALUE[IpccSoilCategory.ORGANIC_SOILS]
|
|
54
|
+
|
|
55
|
+
IS_100_THRESHOLD = 99.5
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _measurement(term_id: str, **kwargs):
|
|
59
|
+
measurement = _new_measurement(term_id)
|
|
60
|
+
return measurement | {
|
|
61
|
+
**{k: v for k, v in kwargs.items()},
|
|
62
|
+
"methodClassification": METHOD
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _SoilTypeDatum(NamedTuple):
|
|
67
|
+
term_id: str
|
|
68
|
+
depth_upper: float
|
|
69
|
+
depth_lower: float
|
|
70
|
+
dates: list[str]
|
|
71
|
+
value: float
|
|
72
|
+
is_organic: bool
|
|
73
|
+
is_complete_depth: bool
|
|
74
|
+
is_standard_depth: bool
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _InventoryKey(NamedTuple):
|
|
78
|
+
depth_upper: float
|
|
79
|
+
depth_lower: float
|
|
80
|
+
date: Optional[str]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_InventoryGroup = dict[str, float]
|
|
84
|
+
|
|
85
|
+
_SoilTypeInventory = dict[_InventoryKey, _InventoryGroup]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_DEFAULT_INVENTORY: _SoilTypeInventory = {
|
|
89
|
+
_InventoryKey(None, None, None): {
|
|
90
|
+
"organicSoils": 0,
|
|
91
|
+
"mineralSoils": 100
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _soil_type_data_to_inventory_keys(datum: _SoilTypeDatum):
|
|
97
|
+
return (
|
|
98
|
+
[_InventoryKey(datum.depth_upper, datum.depth_lower, date) for date in dates]
|
|
99
|
+
if len((dates := datum.dates)) > 0
|
|
100
|
+
else [_InventoryKey(datum.depth_upper, datum.depth_lower, None)]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _extract_soil_type_data(node: dict) -> _SoilTypeDatum:
|
|
105
|
+
depth_upper = node.get("depthUpper")
|
|
106
|
+
depth_lower = node.get("depthLower")
|
|
107
|
+
depth_interval = (depth_upper, depth_lower)
|
|
108
|
+
|
|
109
|
+
return _SoilTypeDatum(
|
|
110
|
+
term_id=node.get("term", {}).get("@id"),
|
|
111
|
+
depth_upper=depth_upper,
|
|
112
|
+
depth_lower=depth_lower,
|
|
113
|
+
dates=node.get("dates", []),
|
|
114
|
+
value=get_node_value(node),
|
|
115
|
+
is_organic=node_lookup_match(node, LOOKUPS["soilType"], TARGET_LOOKUP_VALUE),
|
|
116
|
+
is_complete_depth=all(depth is not None for depth in depth_interval),
|
|
117
|
+
is_standard_depth=depth_interval in STANDARD_DEPTHS,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _classify_soil_type_data(soil_type_data: list[_SoilTypeDatum]):
|
|
122
|
+
"""
|
|
123
|
+
Calculate the values of `organicSoils` and `mineralSoils` from `soilType` measurements for each unique combination
|
|
124
|
+
of depth interval and date.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def classify(inventory: _SoilTypeInventory, datum: _SoilTypeDatum) -> _SoilTypeInventory:
|
|
128
|
+
"""
|
|
129
|
+
Sum the values of organic and mineral `soilType` Measurements by depth interval and date.
|
|
130
|
+
"""
|
|
131
|
+
keys = _soil_type_data_to_inventory_keys(datum)
|
|
132
|
+
|
|
133
|
+
inner_key = ORGANIC_SOILS_TERM_ID if datum.is_organic else MINERAL_SOILS_TERM_ID
|
|
134
|
+
|
|
135
|
+
update_dict = {
|
|
136
|
+
key: (inner := inventory.get(key, {})) | {
|
|
137
|
+
inner_key: min(inner.get(inner_key, 0) + datum.value, 100)
|
|
138
|
+
} for key in keys
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return merge(dict(), inventory, update_dict)
|
|
142
|
+
|
|
143
|
+
inventory = _select_most_complete_groups(reduce(classify, soil_type_data, {}))
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
key: {
|
|
147
|
+
ORGANIC_SOILS_TERM_ID: (org := group.get(ORGANIC_SOILS_TERM_ID, 0)),
|
|
148
|
+
MINERAL_SOILS_TERM_ID: 100 - org
|
|
149
|
+
} for key, group in inventory.items()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _group_keys_by_depth(inventory: _SoilTypeInventory) -> dict[tuple, list[_InventoryKey]]:
|
|
154
|
+
|
|
155
|
+
def group(result: dict[tuple, list[_InventoryKey]], key: _InventoryKey) -> dict[tuple, list[_InventoryKey]]:
|
|
156
|
+
depth_interval = (key.depth_upper, key.depth_lower)
|
|
157
|
+
update_dict = {depth_interval: result.get(depth_interval, []) + [key]}
|
|
158
|
+
return result | update_dict
|
|
159
|
+
|
|
160
|
+
return reduce(group, inventory.keys(), {})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _select_most_complete_groups(inventory: _SoilTypeInventory):
|
|
164
|
+
"""
|
|
165
|
+
For each depth interval, we need to choose the inventory items that have the most complete information.
|
|
166
|
+
|
|
167
|
+
Items should be prioritised in the following order:
|
|
168
|
+
|
|
169
|
+
- If only dated items are available, use dated
|
|
170
|
+
- If only undated items are available, use undated
|
|
171
|
+
- If there are a mix of dated and undated items:
|
|
172
|
+
- If dated items include organic soils measurements, use dated
|
|
173
|
+
- If undated items include organic soils measurements, use undated
|
|
174
|
+
- Otherwise, use dated
|
|
175
|
+
"""
|
|
176
|
+
grouped = _group_keys_by_depth(inventory)
|
|
177
|
+
|
|
178
|
+
def select(result: set[_InventoryKey], keys: list[_InventoryKey]) -> set[_InventoryKey]:
|
|
179
|
+
with_dates, without_dates = split_on_condition(set(keys), lambda k: k.date is not None)
|
|
180
|
+
|
|
181
|
+
with_dates_have_org_value = any(
|
|
182
|
+
(
|
|
183
|
+
ORGANIC_SOILS_TERM_ID in (group := inventory.get(key, {}))
|
|
184
|
+
or group.get(MINERAL_SOILS_TERM_ID, 0) >= IS_100_THRESHOLD
|
|
185
|
+
) for key in with_dates
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
without_dates_have_org_value = any(
|
|
189
|
+
(
|
|
190
|
+
ORGANIC_SOILS_TERM_ID in (group := inventory.get(key, {}))
|
|
191
|
+
or group.get(MINERAL_SOILS_TERM_ID, 0) >= IS_100_THRESHOLD
|
|
192
|
+
) for key in without_dates
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
run_with_dates = (
|
|
196
|
+
with_dates_have_org_value
|
|
197
|
+
or (with_dates and not without_dates_have_org_value)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return result | (with_dates if run_with_dates else without_dates)
|
|
201
|
+
|
|
202
|
+
selected_keys = reduce(select, grouped.values(), set())
|
|
203
|
+
|
|
204
|
+
return {k: v for k, v in inventory.items() if k in selected_keys}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _format_dates(dates: list[str]):
|
|
208
|
+
"""Format a list of datestrings for logging."""
|
|
209
|
+
return " ".join(format_str(date) for date in dates) if isinstance(dates, list) and len(dates) else "None"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_DATUM_KEY_TO_FORMAT_FUNC = {
|
|
213
|
+
"depth_upper": lambda x: format_float(x, "cm"),
|
|
214
|
+
"depth_lower": lambda x: format_float(x, "cm"),
|
|
215
|
+
"dates": _format_dates,
|
|
216
|
+
"value": lambda x: format_float(x, "pct area"),
|
|
217
|
+
"is_organic": format_bool,
|
|
218
|
+
"is_complete_depth": format_bool,
|
|
219
|
+
"is_standard_depth": format_bool,
|
|
220
|
+
}
|
|
221
|
+
DEFAULT_FORMAT_FUNC = format_str
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _format_soil_data(data: list[_SoilTypeDatum]):
|
|
225
|
+
return log_as_table(
|
|
226
|
+
{
|
|
227
|
+
format_str(k): _DATUM_KEY_TO_FORMAT_FUNC.get(k, DEFAULT_FORMAT_FUNC)(v) for k, v in datum._asdict().items()
|
|
228
|
+
} for datum in data
|
|
229
|
+
) if data else "None"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
_FILTER_BY = (
|
|
233
|
+
"is_standard_depth",
|
|
234
|
+
"is_complete_depth"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _filter_data_by_depth_availability(data: list[_SoilTypeDatum]):
|
|
239
|
+
"""
|
|
240
|
+
If measurements with depth available -> discard measurements without depth
|
|
241
|
+
If measurements with standard depth available -> discard non-standard depths
|
|
242
|
+
Else, use measurements with depth
|
|
243
|
+
"""
|
|
244
|
+
return next(
|
|
245
|
+
(
|
|
246
|
+
(filter_, result) for filter_ in _FILTER_BY
|
|
247
|
+
if (result := [datum for datum in data if datum.__getattribute__(filter_)])
|
|
248
|
+
),
|
|
249
|
+
(None, data)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _should_run(site: dict):
|
|
254
|
+
soil_type_nodes = split_nodes_by_dates(
|
|
255
|
+
filter_list_term_type(site.get("measurements", []), TermTermType.SOILTYPE)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
filtered_by, soil_type_data = _filter_data_by_depth_availability(
|
|
259
|
+
[_extract_soil_type_data(node) for node in soil_type_nodes]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
inventory = _classify_soil_type_data(soil_type_data) if soil_type_data else _DEFAULT_INVENTORY
|
|
263
|
+
|
|
264
|
+
should_run = all([
|
|
265
|
+
inventory
|
|
266
|
+
])
|
|
267
|
+
|
|
268
|
+
for term_id in MEASUREMENT_TERM_IDS:
|
|
269
|
+
|
|
270
|
+
logRequirements(
|
|
271
|
+
site,
|
|
272
|
+
model=MODEL,
|
|
273
|
+
term=term_id,
|
|
274
|
+
soil_type_data=_format_soil_data(soil_type_data),
|
|
275
|
+
filtered_by=format_str(filtered_by)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
logShouldRun(site, MODEL, term_id, should_run)
|
|
279
|
+
|
|
280
|
+
return should_run, inventory
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
_INVENTORY_KEY_TO_FIELD_KEY = {
|
|
284
|
+
"depth_upper": "depthUpper",
|
|
285
|
+
"depth_lower": "depthLower",
|
|
286
|
+
"date": "dates"
|
|
287
|
+
}
|
|
288
|
+
_INVENTORY_KEY_TO_FIELD_VALUE = {
|
|
289
|
+
"date": lambda x: [x]
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _key_to_measurement_fields(key: _InventoryKey):
|
|
294
|
+
return {
|
|
295
|
+
_INVENTORY_KEY_TO_FIELD_KEY.get(k, k): _INVENTORY_KEY_TO_FIELD_VALUE.get(k, lambda x: x)(v)
|
|
296
|
+
for k, v in key._asdict().items() if v is not None
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _run(inventory: _SoilTypeInventory) -> list[dict]:
|
|
301
|
+
return flatten(
|
|
302
|
+
[
|
|
303
|
+
_measurement(
|
|
304
|
+
term_id,
|
|
305
|
+
value=[value],
|
|
306
|
+
**_key_to_measurement_fields(key)
|
|
307
|
+
) for term_id, value in value.items()
|
|
308
|
+
] for key, value in inventory.items()
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def run(site: dict):
|
|
313
|
+
should_run, valid_inventory = _should_run(site)
|
|
314
|
+
return _run(valid_inventory) if should_run else []
|
|
@@ -2,7 +2,7 @@ from enum import Enum
|
|
|
2
2
|
from functools import reduce
|
|
3
3
|
from numpy import average, copy, random, vstack
|
|
4
4
|
from numpy.typing import NDArray
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Union
|
|
6
6
|
from hestia_earth.schema import (
|
|
7
7
|
MeasurementMethodClassification,
|
|
8
8
|
MeasurementStatsDefinition,
|
|
@@ -12,7 +12,7 @@ from hestia_earth.utils.tools import non_empty_list
|
|
|
12
12
|
from hestia_earth.utils.stats import gen_seed
|
|
13
13
|
from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
|
|
14
14
|
|
|
15
|
-
from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
|
|
15
|
+
from hestia_earth.models.log import format_bool, format_float, log_as_table, logRequirements, logShouldRun
|
|
16
16
|
from hestia_earth.models.utils import pairwise
|
|
17
17
|
from hestia_earth.models.utils.blank_node import group_nodes_by_year
|
|
18
18
|
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
@@ -338,7 +338,7 @@ def _format_inventory(inventory: dict) -> str:
|
|
|
338
338
|
{
|
|
339
339
|
"year": year,
|
|
340
340
|
**{
|
|
341
|
-
_format_column_header(category):
|
|
341
|
+
_format_column_header(category): format_float(
|
|
342
342
|
inventory.get(year, {}).get(_InventoryKey.LAND_COVER_SUMMARY, {}).get(category, 0)
|
|
343
343
|
) for category in land_covers
|
|
344
344
|
},
|
|
@@ -384,16 +384,6 @@ def _get_loggable_inventory_keys(inventory: dict) -> list:
|
|
|
384
384
|
return sorted(unique_keys, key=lambda key_: key_order[key_])
|
|
385
385
|
|
|
386
386
|
|
|
387
|
-
def _format_bool(value: Optional[bool]) -> str:
|
|
388
|
-
"""Format a bool for logging in a table."""
|
|
389
|
-
return str(bool(value))
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def _format_number(value: Optional[float]) -> str:
|
|
393
|
-
"""Format a float for logging in a table."""
|
|
394
|
-
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
395
|
-
|
|
396
|
-
|
|
397
387
|
def _format_column_header(value: Union[_InventoryKey, BiomassCategory, str]):
|
|
398
388
|
"""Format an enum or str for logging as a table column header."""
|
|
399
389
|
as_string = value.value if isinstance(value, Enum) else str(value)
|
|
@@ -401,8 +391,8 @@ def _format_column_header(value: Union[_InventoryKey, BiomassCategory, str]):
|
|
|
401
391
|
|
|
402
392
|
|
|
403
393
|
_INVENTORY_KEY_TO_FORMAT_FUNC = {
|
|
404
|
-
_InventoryKey.LAND_COVER_CHANGE_EVENT:
|
|
405
|
-
_InventoryKey.YEARS_SINCE_LCC_EVENT:
|
|
394
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: format_bool,
|
|
395
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: format_float
|
|
406
396
|
}
|
|
407
397
|
"""
|
|
408
398
|
Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
|
|
@@ -2,7 +2,7 @@ from enum import Enum
|
|
|
2
2
|
from functools import reduce
|
|
3
3
|
from numpy import average, copy, random, vstack
|
|
4
4
|
from numpy.typing import NDArray
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Union
|
|
6
6
|
from hestia_earth.schema import (
|
|
7
7
|
MeasurementMethodClassification,
|
|
8
8
|
MeasurementStatsDefinition,
|
|
@@ -12,7 +12,7 @@ from hestia_earth.utils.tools import non_empty_list
|
|
|
12
12
|
from hestia_earth.utils.stats import gen_seed
|
|
13
13
|
from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
|
|
14
14
|
|
|
15
|
-
from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
|
|
15
|
+
from hestia_earth.models.log import format_bool, format_float, log_as_table, logRequirements, logShouldRun
|
|
16
16
|
from hestia_earth.models.utils import pairwise
|
|
17
17
|
from hestia_earth.models.utils.blank_node import group_nodes_by_year
|
|
18
18
|
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
@@ -325,7 +325,7 @@ def _format_inventory(inventory: dict) -> str:
|
|
|
325
325
|
{
|
|
326
326
|
"year": year,
|
|
327
327
|
**{
|
|
328
|
-
_format_column_header(category):
|
|
328
|
+
_format_column_header(category): format_float(
|
|
329
329
|
inventory.get(year, {}).get(_InventoryKey.BIOMASS_CATEGORY_SUMMARY, {}).get(category, 0)
|
|
330
330
|
) for category in land_covers
|
|
331
331
|
},
|
|
@@ -371,16 +371,6 @@ def _get_loggable_inventory_keys(inventory: dict) -> list:
|
|
|
371
371
|
return sorted(unique_keys, key=lambda key_: key_order[key_])
|
|
372
372
|
|
|
373
373
|
|
|
374
|
-
def _format_bool(value: Optional[bool]) -> str:
|
|
375
|
-
"""Format a bool for logging in a table."""
|
|
376
|
-
return str(bool(value))
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def _format_number(value: Optional[float]) -> str:
|
|
380
|
-
"""Format a float for logging in a table."""
|
|
381
|
-
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
382
|
-
|
|
383
|
-
|
|
384
374
|
def _format_column_header(value: Union[_InventoryKey, BiomassCategory, str]):
|
|
385
375
|
"""Format an enum or str for logging as a table column header."""
|
|
386
376
|
as_string = value.value if isinstance(value, Enum) else str(value)
|
|
@@ -388,8 +378,8 @@ def _format_column_header(value: Union[_InventoryKey, BiomassCategory, str]):
|
|
|
388
378
|
|
|
389
379
|
|
|
390
380
|
_INVENTORY_KEY_TO_FORMAT_FUNC = {
|
|
391
|
-
_InventoryKey.LAND_COVER_CHANGE_EVENT:
|
|
392
|
-
_InventoryKey.YEARS_SINCE_LCC_EVENT:
|
|
381
|
+
_InventoryKey.LAND_COVER_CHANGE_EVENT: format_bool,
|
|
382
|
+
_InventoryKey.YEARS_SINCE_LCC_EVENT: format_float
|
|
393
383
|
}
|
|
394
384
|
"""
|
|
395
385
|
Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
|
|
@@ -11,7 +11,7 @@ from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
|
|
|
11
11
|
from hestia_earth.utils.stats import gen_seed, truncated_normal_1d
|
|
12
12
|
from hestia_earth.utils.tools import non_empty_list
|
|
13
13
|
|
|
14
|
-
from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
|
|
14
|
+
from hestia_earth.models.log import format_nd_array, format_str, log_as_table, logRequirements, logShouldRun
|
|
15
15
|
from hestia_earth.models.utils.blank_node import group_nodes_by_year, filter_list_term_type
|
|
16
16
|
from hestia_earth.models.utils.measurement import _new_measurement
|
|
17
17
|
from hestia_earth.models.utils.property import get_node_property
|
|
@@ -280,17 +280,17 @@ def _format_inventory(inventory: dict) -> str:
|
|
|
280
280
|
return log_as_table(
|
|
281
281
|
{
|
|
282
282
|
"year": year,
|
|
283
|
-
"stable-oc-from-biochar":
|
|
283
|
+
"stable-oc-from-biochar": format_nd_array(inventory.get(year))
|
|
284
284
|
} for year in inventory_years
|
|
285
285
|
) if should_run else "None"
|
|
286
286
|
|
|
287
287
|
|
|
288
288
|
def _format_logs(logs: dict):
|
|
289
289
|
"""
|
|
290
|
-
Format model logs. Format method selected based on dict key, with `
|
|
290
|
+
Format model logs. Format method selected based on dict key, with `format_str` as fallback.
|
|
291
291
|
"""
|
|
292
292
|
return {
|
|
293
|
-
|
|
293
|
+
format_str(key): _LOG_KEY_TO_FORMAT_FUNC.get(key, format_str)(value) for key, value in logs.items()
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
|
|
@@ -303,45 +303,11 @@ def _format_factor_cache(factor_cache: dict) -> str:
|
|
|
303
303
|
return log_as_table(
|
|
304
304
|
{
|
|
305
305
|
"term-id": term_id,
|
|
306
|
-
**{
|
|
306
|
+
**{format_str(key): format_nd_array(value) for key, value in factor_dict.items()}
|
|
307
307
|
} for term_id, factor_dict in factor_cache.items()
|
|
308
308
|
) if should_run else "None"
|
|
309
309
|
|
|
310
310
|
|
|
311
|
-
def _format_factor(value) -> str:
|
|
312
|
-
"""
|
|
313
|
-
Format a model factor. Method selected based on factor type (ndarray, int or float).
|
|
314
|
-
"""
|
|
315
|
-
format_func = next(
|
|
316
|
-
(func for type, func in _TYPE_TO_FORMAT_FUNC.items() if isinstance(value, type)),
|
|
317
|
-
None
|
|
318
|
-
)
|
|
319
|
-
return format_func(value) if format_func else "None"
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _format_nd_array(value) -> str:
|
|
323
|
-
return f"{np.mean(value):.3g} ± {np.std(value):.3g}"
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
def _format_number(value) -> str:
|
|
327
|
-
return f"{value:.3g}"
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
_INVALID_CHARS = {"_", ":", ",", "="}
|
|
331
|
-
_REPLACEMENT_CHAR = "-"
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
def _format_str(value: str, *_) -> str:
|
|
335
|
-
"""Format a string for logging in a table. Remove all characters used to render the table on the front end."""
|
|
336
|
-
return reduce(lambda x, char: x.replace(char, _REPLACEMENT_CHAR), _INVALID_CHARS, str(value))
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
_TYPE_TO_FORMAT_FUNC = {
|
|
340
|
-
np.ndarray: _format_nd_array,
|
|
341
|
-
(float, int): _format_number
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
311
|
_LOG_KEY_TO_FORMAT_FUNC = {
|
|
346
312
|
"factor_cache": _format_factor_cache
|
|
347
313
|
}
|
|
@@ -74,7 +74,8 @@ LOOKUPS = {
|
|
|
74
74
|
],
|
|
75
75
|
"crop-property": ["neutralDetergentFibreContent", "energyContentHigherHeatingValue"],
|
|
76
76
|
"forage-property": ["neutralDetergentFibreContent", "energyContentHigherHeatingValue"],
|
|
77
|
-
"processedFood-property": ["neutralDetergentFibreContent", "energyContentHigherHeatingValue"]
|
|
77
|
+
"processedFood-property": ["neutralDetergentFibreContent", "energyContentHigherHeatingValue"],
|
|
78
|
+
"feedFoodAdditive": ["hasEnergyContent"]
|
|
78
79
|
}
|
|
79
80
|
RETURNS = {
|
|
80
81
|
"Emission": [{
|
|
@@ -2,7 +2,7 @@ import numpy as np
|
|
|
2
2
|
import numpy.typing as npt
|
|
3
3
|
from typing import Callable, Union
|
|
4
4
|
from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition
|
|
5
|
-
from hestia_earth.models.log import logRequirements, logShouldRun
|
|
5
|
+
from hestia_earth.models.log import format_nd_array, format_float, logRequirements, logShouldRun
|
|
6
6
|
from hestia_earth.utils.stats import (
|
|
7
7
|
discrete_uniform_1d, gen_seed, normal_1d, repeat_single, triangular_1d
|
|
8
8
|
)
|
|
@@ -15,8 +15,8 @@ from hestia_earth.models.utils.measurement import most_relevant_measurement_valu
|
|
|
15
15
|
from hestia_earth.models.utils.site import valid_site_type
|
|
16
16
|
|
|
17
17
|
from .organicSoilCultivation_utils import (
|
|
18
|
-
assign_ditch_category, assign_organic_soil_category, calc_emission, DitchCategory,
|
|
19
|
-
|
|
18
|
+
assign_ditch_category, assign_organic_soil_category, calc_emission, DitchCategory, get_ditch_frac,
|
|
19
|
+
get_emission_factor, OrganicSoilCategory, remap_categories, valid_eco_climate_zone
|
|
20
20
|
)
|
|
21
21
|
from . import MODEL
|
|
22
22
|
|
|
@@ -226,8 +226,8 @@ def _should_run(cycle: dict):
|
|
|
226
226
|
emission_factor=format_nd_array(emission_factor),
|
|
227
227
|
ditch_factor=format_nd_array(ditch_factor),
|
|
228
228
|
ditch_frac=format_nd_array(ditch_frac),
|
|
229
|
-
land_occupation=
|
|
230
|
-
histosol=
|
|
229
|
+
land_occupation=format_float(land_occupation),
|
|
230
|
+
histosol=format_float(histosol)
|
|
231
231
|
)
|
|
232
232
|
|
|
233
233
|
should_run = all([
|
|
@@ -18,7 +18,7 @@ from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
|
|
|
18
18
|
from hestia_earth.utils.stats import correlated_normal_2d, gen_seed
|
|
19
19
|
from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
|
|
20
20
|
|
|
21
|
-
from hestia_earth.models.log import log_as_table
|
|
21
|
+
from hestia_earth.models.log import format_bool, format_float, format_int, format_nd_array, log_as_table
|
|
22
22
|
from hestia_earth.models.utils import pairwise
|
|
23
23
|
from hestia_earth.models.utils.blank_node import (
|
|
24
24
|
_gapfill_datestr, _get_datestr_format, cumulative_nodes_term_match, DatestrGapfillMode, DatestrFormat,
|
|
@@ -1386,7 +1386,7 @@ def _format_cycle_inventory(cycle_inventory: dict) -> str:
|
|
|
1386
1386
|
{
|
|
1387
1387
|
"year": year,
|
|
1388
1388
|
**{
|
|
1389
|
-
id:
|
|
1389
|
+
id: format_float(group.get(KEY, {}).get(id, 0)) for id in unique_cycles
|
|
1390
1390
|
}
|
|
1391
1391
|
} for year, group in cycle_inventory.items()
|
|
1392
1392
|
) if should_run else "None"
|
|
@@ -1450,21 +1450,6 @@ def _format_land_use_inventory(land_use_inventory: dict) -> str:
|
|
|
1450
1450
|
) if should_run else "None"
|
|
1451
1451
|
|
|
1452
1452
|
|
|
1453
|
-
def _format_bool(value: Optional[bool]) -> str:
|
|
1454
|
-
"""Format a bool for logging in a table."""
|
|
1455
|
-
return str(value) if isinstance(value, bool) else "None"
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
def _format_int(value: Optional[float]) -> str:
|
|
1459
|
-
"""Format an int for logging in a table. If the value is invalid, return `"None"` as a string."""
|
|
1460
|
-
return f"{value:.0f}" if isinstance(value, (float, int)) else "None"
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
def _format_number(value: Optional[float]) -> str:
|
|
1464
|
-
"""Format a float for logging in a table. If the value is invalid, return `"None"` as a string."""
|
|
1465
|
-
return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
1453
|
def _format_column_header(method: MeasurementMethodClassification, inventory_key: _InventoryKey) -> str:
|
|
1469
1454
|
"""
|
|
1470
1455
|
Format a measurement method classification and inventory key for logging in a table as a column header. Replace any
|
|
@@ -1483,16 +1468,16 @@ def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, Ca
|
|
|
1483
1468
|
Extract and format just the value and discard the other data. If the value is invalid, return `"None"` as a string.
|
|
1484
1469
|
"""
|
|
1485
1470
|
return (
|
|
1486
|
-
|
|
1471
|
+
format_nd_array(mean(value.value))
|
|
1487
1472
|
if isinstance(value, (CarbonStock, CarbonStockChange, CarbonStockChangeEmission))
|
|
1488
1473
|
else "None"
|
|
1489
1474
|
)
|
|
1490
1475
|
|
|
1491
1476
|
|
|
1492
1477
|
_LAND_USE_INVENTORY_KEY_TO_FORMAT_FUNC = {
|
|
1493
|
-
_InventoryKey.LAND_USE_CHANGE_EVENT:
|
|
1494
|
-
_InventoryKey.YEARS_SINCE_LUC_EVENT:
|
|
1495
|
-
_InventoryKey.YEARS_SINCE_INVENTORY_START:
|
|
1478
|
+
_InventoryKey.LAND_USE_CHANGE_EVENT: format_bool,
|
|
1479
|
+
_InventoryKey.YEARS_SINCE_LUC_EVENT: format_int,
|
|
1480
|
+
_InventoryKey.YEARS_SINCE_INVENTORY_START: format_int
|
|
1496
1481
|
}
|
|
1497
1482
|
"""
|
|
1498
1483
|
Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
|
|
@@ -5,7 +5,7 @@ from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition
|
|
|
5
5
|
from hestia_earth.utils.stats import gen_seed, repeat_single, truncated_normal_1d
|
|
6
6
|
from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
|
|
7
7
|
|
|
8
|
-
from hestia_earth.models.log import logRequirements, logShouldRun
|
|
8
|
+
from hestia_earth.models.log import format_float, format_nd_array, logRequirements, logShouldRun
|
|
9
9
|
from hestia_earth.models.utils.cycle import land_occupation_per_ha
|
|
10
10
|
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
11
11
|
from hestia_earth.models.utils.emission import _new_emission
|
|
@@ -13,8 +13,7 @@ from hestia_earth.models.utils.measurement import most_relevant_measurement_valu
|
|
|
13
13
|
from hestia_earth.models.utils.site import valid_site_type
|
|
14
14
|
|
|
15
15
|
from .organicSoilCultivation_utils import (
|
|
16
|
-
assign_organic_soil_category, calc_emission,
|
|
17
|
-
OrganicSoilCategory, valid_eco_climate_zone
|
|
16
|
+
assign_organic_soil_category, calc_emission, get_emission_factor, OrganicSoilCategory, valid_eco_climate_zone
|
|
18
17
|
)
|
|
19
18
|
from . import MODEL
|
|
20
19
|
|
|
@@ -183,8 +182,8 @@ def _should_run(cycle: dict):
|
|
|
183
182
|
eco_climate_zone=eco_climate_zone,
|
|
184
183
|
organic_soil_category=organic_soil_category,
|
|
185
184
|
emission_factor=format_nd_array(emission_factor),
|
|
186
|
-
land_occupation=
|
|
187
|
-
histosol=
|
|
185
|
+
land_occupation=format_float(land_occupation),
|
|
186
|
+
histosol=format_float(histosol)
|
|
188
187
|
)
|
|
189
188
|
|
|
190
189
|
should_run = all([
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from hestia_earth.schema import EmissionMethodTier
|
|
2
2
|
|
|
3
|
-
from hestia_earth.models.log import logRequirements, logShouldRun
|
|
3
|
+
from hestia_earth.models.log import format_float, logRequirements, logShouldRun
|
|
4
4
|
from hestia_earth.models.utils.cycle import land_occupation_per_ha
|
|
5
5
|
from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
|
|
6
6
|
from hestia_earth.models.utils.emission import _new_emission
|
|
@@ -8,7 +8,7 @@ from hestia_earth.models.utils.measurement import most_relevant_measurement_valu
|
|
|
8
8
|
from hestia_earth.models.utils.site import valid_site_type
|
|
9
9
|
|
|
10
10
|
from .organicSoilCultivation_utils import (
|
|
11
|
-
assign_organic_soil_category, calc_emission,
|
|
11
|
+
assign_organic_soil_category, calc_emission, get_emission_factor, OrganicSoilCategory,
|
|
12
12
|
remap_categories, valid_eco_climate_zone
|
|
13
13
|
)
|
|
14
14
|
from . import MODEL
|
|
@@ -122,9 +122,9 @@ def _should_run(cycle: dict):
|
|
|
122
122
|
cycle, model=MODEL, term=TERM_ID,
|
|
123
123
|
eco_climate_zone=eco_climate_zone,
|
|
124
124
|
organic_soil_category=organic_soil_category,
|
|
125
|
-
emission_factor=f"{
|
|
126
|
-
land_occupation=
|
|
127
|
-
histosol=
|
|
125
|
+
emission_factor=f"{format_float(emission_factor_mean)} ± {format_float(emission_factor_sd)}",
|
|
126
|
+
land_occupation=format_float(land_occupation),
|
|
127
|
+
histosol=format_float(histosol)
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
should_run = all([
|