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.

Files changed (62) hide show
  1. hestia_earth/models/blonkConsultants2016/ch4ToAirNaturalVegetationBurning.py +5 -9
  2. hestia_earth/models/blonkConsultants2016/co2ToAirAboveGroundBiomassStockChangeLandUseChange.py +5 -9
  3. hestia_earth/models/blonkConsultants2016/n2OToAirNaturalVegetationBurningDirect.py +6 -13
  4. hestia_earth/models/cycle/animal/input/properties.py +6 -0
  5. hestia_earth/models/cycle/completeness/soilAmendment.py +3 -2
  6. hestia_earth/models/cycle/concentrateFeed.py +10 -4
  7. hestia_earth/models/cycle/input/properties.py +6 -0
  8. hestia_earth/models/cycle/liveAnimal.py +2 -2
  9. hestia_earth/models/cycle/milkYield.py +3 -3
  10. hestia_earth/models/cycle/otherSitesArea.py +59 -0
  11. hestia_earth/models/cycle/otherSitesUnusedDuration.py +9 -8
  12. hestia_earth/models/cycle/pastureSystem.py +3 -2
  13. hestia_earth/models/cycle/product/properties.py +6 -0
  14. hestia_earth/models/cycle/siteArea.py +83 -0
  15. hestia_earth/models/cycle/stockingDensityAnimalHousingAverage.py +28 -16
  16. hestia_earth/models/cycle/utils.py +1 -1
  17. hestia_earth/models/environmentalFootprintV3/soilQualityIndexLandOccupation.py +128 -0
  18. hestia_earth/models/environmentalFootprintV3/utils.py +17 -0
  19. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +17 -6
  20. hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +17 -6
  21. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +904 -0
  22. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +70 -618
  23. hestia_earth/models/mocking/search-results.json +395 -323
  24. hestia_earth/models/pooreNemecek2018/saplings.py +10 -7
  25. hestia_earth/models/site/management.py +18 -14
  26. hestia_earth/models/utils/__init__.py +38 -0
  27. hestia_earth/models/utils/array_builders.py +63 -52
  28. hestia_earth/models/utils/blank_node.py +137 -82
  29. hestia_earth/models/utils/descriptive_stats.py +3 -239
  30. hestia_earth/models/utils/feedipedia.py +15 -2
  31. hestia_earth/models/utils/landCover.py +9 -0
  32. hestia_earth/models/utils/lookup.py +13 -2
  33. hestia_earth/models/utils/measurement.py +3 -28
  34. hestia_earth/models/utils/stats.py +429 -0
  35. hestia_earth/models/utils/term.py +15 -3
  36. hestia_earth/models/utils/time_series.py +90 -0
  37. hestia_earth/models/version.py +1 -1
  38. {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/METADATA +1 -1
  39. {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/RECORD +62 -48
  40. tests/models/blonkConsultants2016/test_ch4ToAirNaturalVegetationBurning.py +2 -2
  41. tests/models/blonkConsultants2016/test_co2ToAirAboveGroundBiomassStockChangeLandUseChange.py +2 -2
  42. tests/models/blonkConsultants2016/test_n2OToAirNaturalVegetationBurningDirect.py +2 -2
  43. tests/models/cycle/completeness/test_soilAmendment.py +1 -1
  44. tests/models/cycle/test_liveAnimal.py +1 -1
  45. tests/models/cycle/test_milkYield.py +1 -1
  46. tests/models/cycle/test_otherSitesArea.py +68 -0
  47. tests/models/cycle/test_siteArea.py +51 -0
  48. tests/models/cycle/test_stockingDensityAnimalHousingAverage.py +2 -2
  49. tests/models/environmentalFootprintV3/test_soilQualityIndexLandOccupation.py +136 -0
  50. tests/models/ipcc2019/test_co2ToAirCarbonStockChange_utils.py +50 -0
  51. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +1 -39
  52. tests/models/pooreNemecek2018/test_saplings.py +1 -1
  53. tests/models/site/test_management.py +3 -153
  54. tests/models/utils/test_array_builders.py +67 -6
  55. tests/models/utils/test_blank_node.py +191 -7
  56. tests/models/utils/test_descriptive_stats.py +2 -86
  57. tests/models/utils/test_measurement.py +1 -22
  58. tests/models/utils/test_stats.py +186 -0
  59. tests/models/utils/test_time_series.py +88 -0
  60. {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/LICENSE +0 -0
  61. {hestia_earth_models-0.64.4.dist-info → hestia_earth_models-0.64.5.dist-info}/WHEEL +0 -0
  62. {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 itertools import product
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 log_as_table, logRequirements, logShouldRun
16
- from hestia_earth.models.utils import pairwise
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, _gapfill_datestr, DatestrGapfillMode, DatestrFormat, group_nodes_by_year, node_term_match,
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
- DEPTH_UPPER = 0
63
- DEPTH_LOWER = 30
53
+ _DEPTH_UPPER = 0
54
+ _DEPTH_LOWER = 30
55
+ _ITERATIONS = 10000
64
56
 
65
- ORGANIC_CARBON_PER_HA_TERM_ID = 'organicCarbonPerHa'
57
+ _ORGANIC_CARBON_PER_HA_TERM_ID = 'organicCarbonPerHa'
66
58
 
67
- VALID_DATE_FORMATS = {
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
- VALID_MEASUREMENT_METHOD_CLASSIFICATIONS = [
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
- class _InventoryKey(Enum):
95
- """
96
- The inner keys of the annualised inventory created by the `_compile_inventory` function.
97
-
98
- The value of each enum member is formatted to be used as a column header in the `log_as_table` function.
99
- """
100
- SOC_STOCK = "soc-stock"
101
- SOC_STOCK_CHANGE = "soc-stock-change"
102
- CO2_EMISSION = "co2-emission"
103
- SHARE_OF_EMISSION = "share-of-emissions"
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
- emission = _new_emission(TERM_ID, MODEL)
192
- emission["depth"] = DEPTH_LOWER
193
- emission["value"] = [value]
194
- emission["methodTier"] = method_tier.value
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
- _compile_inventory(cycle_id, cycles, soc_measurements) if should_compile_inventory else ({}, {})
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, ORGANIC_CARBON_PER_HA_TERM_ID),
298
- node.get("depthUpper") == DEPTH_UPPER,
299
- node.get("depthLower") == DEPTH_LOWER,
300
- node.get("methodClassification") in (m.value for m in VALID_MEASUREMENT_METHOD_CLASSIFICATIONS),
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
- all(_get_datestr_format(datestr) in VALID_DATE_FORMATS for datestr in dates)
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
- total_emission = _sum_soc_stock_change_emissions([
784
- _rescale_stock_change_emission(
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
- Returns
824
- -------
825
- SocStockChangeEmission
826
- The summed emission.
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
- return SocStockChangeEmission(value, start_date, end_date, method)
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]: