hestia-earth-models 0.74.15__py3-none-any.whl → 0.74.16__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.

@@ -18,7 +18,10 @@ 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 format_bool, format_float, format_int, format_nd_array, log_as_table
21
+ from hestia_earth.models.log import (
22
+ format_conditional_message, format_bool, format_float, format_int, format_nd_array, log_as_table, logRequirements,
23
+ logShouldRun
24
+ )
22
25
  from hestia_earth.models.utils import pairwise
23
26
  from hestia_earth.models.utils.blank_node import (
24
27
  _gapfill_datestr, _get_datestr_format, cumulative_nodes_term_match, DatestrGapfillMode, DatestrFormat,
@@ -88,6 +91,11 @@ _SITE_TYPE_SYSTEMS_MAPPING = {
88
91
  ]
89
92
  }
90
93
 
94
+ _MEASUREMENTS_REQUIRED_LOG_MESSAGE = {
95
+ "on_true": "True - carbon stock measurements are required for this model to run",
96
+ "on_false": "False - this model can run without carbon stock measurements in specific cases (see documentation)"
97
+ }
98
+
91
99
 
92
100
  class _InventoryKey(Enum):
93
101
  """
@@ -103,6 +111,7 @@ class _InventoryKey(Enum):
103
111
  LAND_USE_CHANGE_EVENT = "luc-event"
104
112
  YEARS_SINCE_LUC_EVENT = "years-since-luc-event"
105
113
  YEARS_SINCE_INVENTORY_START = "years-since-inventory-start"
114
+ YEAR_IS_RELEVANT = "year-is-relevant"
106
115
 
107
116
 
108
117
  CarbonStock = NamedTuple("CarbonStock", [
@@ -338,15 +347,18 @@ def _add_carbon_stock_change_emissions(
338
347
 
339
348
  def create_should_run_function(
340
349
  carbon_stock_term_id: str,
350
+ land_use_change_emission_term_id: str,
351
+ management_change_emission_term_id: str,
341
352
  *,
342
353
  depth_upper: Optional[float] = None,
343
354
  depth_lower: Optional[float] = None,
344
- measurements_mandatory: bool = True,
355
+ measurements_required: bool = True,
345
356
  measurement_method_ranking: list[MeasurementMethodClassification] = DEFAULT_MEASUREMENT_METHOD_RANKING,
346
357
  transition_period: float = _TRANSITION_PERIOD_DAYS,
347
358
  get_valid_management_nodes_func: Callable[[dict], list[dict]] = lambda *_: [],
348
359
  summarise_land_use_func: Callable[[list[dict]], Any] = lambda *_: None,
349
- detect_land_use_change_func: Callable[[Any, Any], bool] = lambda *_: False
360
+ detect_land_use_change_func: Callable[[Any, Any], bool] = lambda *_: False,
361
+ exclude_from_logs: Optional[list[str]] = None
350
362
  ) -> Callable[[dict], tuple[bool, str, dict, dict]]:
351
363
  """
352
364
  Create a `should_run` function for a carbon stock change model.
@@ -362,6 +374,12 @@ def create_should_run_function(
362
374
  The `term.@id` of the carbon stock measurement (e.g., `aboveGroundBiomass`, `belowGroundBiomass`,
363
375
  `organicCarbonPerHa`).
364
376
 
377
+ land_use_change_emission_term_id : str
378
+ The term id for emissions allocated to land use changes.
379
+
380
+ management_change_emission_term_id : str
381
+ The term id for emissions allocated to management changes.
382
+
365
383
  depth_upper : float, optional
366
384
  The upper bound of the measurement depth (e.g., 0 cm). If provided, only measurements matching this bound are
367
385
  included.
@@ -370,7 +388,7 @@ def create_should_run_function(
370
388
  The lower bound of the measurement depth (e.g., 30 cm). If provided, only measurements matching this bound are
371
389
  included.
372
390
 
373
- measurements_mandatory : bool, default=True
391
+ measurements_required : bool, default=True
374
392
  If `True`, at least two valid measurement must be present for the cycle to be included in the inventory. If
375
393
  `False`, the function may allow an inventory to be generated without direct measurements.
376
394
 
@@ -407,6 +425,10 @@ def create_should_run_function(
407
425
 
408
426
  Detects whether a land use change event has occurred between two summaries.
409
427
 
428
+ exclude_from_logs : list[str], optional
429
+
430
+ A list of log keys to exclude from the model logs.
431
+
410
432
  Returns
411
433
  -------
412
434
  should_run_func : Callable[[dict], tuple[bool, str, dict, dict]]
@@ -471,7 +493,7 @@ def create_should_run_function(
471
493
  has_soil
472
494
  and has_cycles
473
495
  and has_functional_unit_1_ha
474
- and (has_stock_measurements or not measurements_mandatory)
496
+ and (has_stock_measurements or not measurements_required)
475
497
  )
476
498
 
477
499
  compile_inventory_func = _create_compile_inventory_function(
@@ -485,13 +507,21 @@ def create_should_run_function(
485
507
 
486
508
  inventory, inventory_logs = (
487
509
  compile_inventory_func(
510
+ cycle_id,
488
511
  cycles,
489
512
  carbon_stock_measurements,
490
513
  land_cover_nodes
491
514
  ) if should_compile_inventory else ({}, {})
492
515
  )
493
516
 
494
- has_valid_inventory = len(inventory) > 0
517
+ assigned_emissions = _assign_emissions(
518
+ cycle_id,
519
+ inventory,
520
+ land_use_change_emission_term_id,
521
+ management_change_emission_term_id
522
+ )
523
+
524
+ has_valid_inventory = bool(assigned_emissions) > 0
495
525
  has_consecutive_years = check_consecutive(inventory.keys())
496
526
 
497
527
  should_run_ = all([has_valid_inventory, has_consecutive_years])
@@ -506,10 +536,28 @@ def create_should_run_function(
506
536
  "has_valid_inventory": has_valid_inventory,
507
537
  "has_consecutive_years": has_consecutive_years,
508
538
  "has_stock_measurements": has_stock_measurements,
509
- "measurements_mandatory": measurements_mandatory
539
+ "measurements_required": format_conditional_message(
540
+ measurements_required,
541
+ **_MEASUREMENTS_REQUIRED_LOG_MESSAGE
542
+ )
510
543
  }
511
544
 
512
- return should_run_, cycle_id, inventory, logs
545
+ final_logs = _filter_logs(logs, exclude_from_logs) if isinstance(exclude_from_logs, list) else logs
546
+
547
+ for term_id in [land_use_change_emission_term_id, management_change_emission_term_id]:
548
+
549
+ assigned_emission = assigned_emissions.get(term_id)
550
+ tier = (
551
+ _get_emission_method(assigned_emission).value if assigned_emission
552
+ else _DEFAULT_EMISSION_METHOD_TIER.value
553
+ )
554
+
555
+ should_run_term = should_run_ and bool(assigned_emission)
556
+
557
+ logRequirements(cycle, model=MODEL, term=term_id, **final_logs)
558
+ logShouldRun(cycle, MODEL, term_id, should_run_term, methodTier=tier)
559
+
560
+ return should_run_, assigned_emissions
513
561
 
514
562
  return should_run
515
563
 
@@ -589,6 +637,7 @@ def _create_compile_inventory_function(
589
637
  A function that compiles an annual inventory given cycles, carbon stock measurements, and land cover nodes.
590
638
  """
591
639
  def compile_inventory(
640
+ cycle_id: str,
592
641
  cycles: list[dict],
593
642
  carbon_stock_measurements: list[dict],
594
643
  land_cover_nodes: list[dict]
@@ -612,6 +661,9 @@ def _create_compile_inventory_function(
612
661
 
613
662
  Parameters
614
663
  ----------
664
+ cycle_id : str
665
+ The `@id` of the cycle the model is running on.
666
+
615
667
  cycles : list[dict]
616
668
  A list of [Cycle](https://www.hestia.earth/schema/Cycles) nodes related to the site.
617
669
 
@@ -645,7 +697,7 @@ def _create_compile_inventory_function(
645
697
  logs : dict
646
698
  Diagnostic logs describing intermediate steps and validation decisions.
647
699
  """
648
- cycle_inventory = _compile_cycle_inventory(cycles)
700
+ cycle_inventory = _compile_cycle_inventory(cycle_id, cycles)
649
701
  carbon_stock_inventory = _compile_carbon_stock_inventory(
650
702
  carbon_stock_measurements, transition_period=transition_period, iterations=iterations, seed=seed
651
703
  )
@@ -667,7 +719,7 @@ def _create_compile_inventory_function(
667
719
  return compile_inventory
668
720
 
669
721
 
670
- def _compile_cycle_inventory(cycles: list[dict]) -> dict:
722
+ def _compile_cycle_inventory(cycle_id: str, cycles: list[dict]) -> dict:
671
723
  """
672
724
  Compile the share of emissions for each cycle, grouped by inventory year.
673
725
 
@@ -690,6 +742,9 @@ def _compile_cycle_inventory(cycles: list[dict]) -> dict:
690
742
 
691
743
  Parameters
692
744
  ----------
745
+ cycle_id : str
746
+ The `@id` of the cycle the model is running on.
747
+
693
748
  cycles : list[dict]
694
749
  List of [Cycle](https://www.hestia.earth/schema/Cycle) nodes.
695
750
 
@@ -713,8 +768,10 @@ def _compile_cycle_inventory(cycles: list[dict]) -> dict:
713
768
  }
714
769
 
715
770
  return {
716
- year: {_InventoryKey.SHARE_OF_EMISSION: calculate_emissions(cycles_in_year)}
717
- for year, cycles_in_year in grouped_cycles.items()
771
+ year: {
772
+ _InventoryKey.SHARE_OF_EMISSION: calculate_emissions(cycles_in_year),
773
+ _InventoryKey.YEAR_IS_RELEVANT: cycle_id in (cycle.get("@id") for cycle in cycles_in_year)
774
+ } for year, cycles_in_year in grouped_cycles.items()
718
775
  }
719
776
 
720
777
 
@@ -1373,11 +1430,12 @@ def _format_cycle_inventory(cycle_inventory: dict) -> str:
1373
1430
  Format the cycle inventory for logging as a table. Rows represent inventory years, columns represent the share of
1374
1431
  emission for each cycle present in the inventory. If the inventory is invalid, return `"None"` as a string.
1375
1432
  """
1376
- KEY = _InventoryKey.SHARE_OF_EMISSION
1433
+ RELEVANT_KEY = _InventoryKey.YEAR_IS_RELEVANT
1434
+ SHARE_KEY = _InventoryKey.SHARE_OF_EMISSION
1377
1435
 
1378
1436
  unique_cycles = sorted(
1379
- set(non_empty_list(flatten(list(group[KEY]) for group in cycle_inventory.values()))),
1380
- key=lambda id: next((year, id) for year in cycle_inventory if id in cycle_inventory[year][KEY])
1437
+ set(non_empty_list(flatten(list(group[SHARE_KEY]) for group in cycle_inventory.values()))),
1438
+ key=lambda id: next((year, id) for year in cycle_inventory if id in cycle_inventory[year][SHARE_KEY])
1381
1439
  )
1382
1440
 
1383
1441
  should_run = cycle_inventory and len(unique_cycles) > 0
@@ -1385,8 +1443,9 @@ def _format_cycle_inventory(cycle_inventory: dict) -> str:
1385
1443
  return log_as_table(
1386
1444
  {
1387
1445
  "year": year,
1446
+ RELEVANT_KEY.value: format_bool(group.get(RELEVANT_KEY, False)),
1388
1447
  **{
1389
- id: format_float(group.get(KEY, {}).get(id, 0)) for id in unique_cycles
1448
+ id: format_float(group.get(SHARE_KEY, {}).get(id, 0)) for id in unique_cycles
1390
1449
  }
1391
1450
  } for year, group in cycle_inventory.items()
1392
1451
  ) if should_run else "None"
@@ -1474,6 +1533,10 @@ def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, Ca
1474
1533
  )
1475
1534
 
1476
1535
 
1536
+ def _filter_logs(logs: dict[str, Any], exclude_keys: list[str]):
1537
+ return {k: v for k, v in logs.items() if k not in exclude_keys}
1538
+
1539
+
1477
1540
  _LAND_USE_INVENTORY_KEY_TO_FORMAT_FUNC = {
1478
1541
  _InventoryKey.LAND_USE_CHANGE_EVENT: format_bool,
1479
1542
  _InventoryKey.YEARS_SINCE_LUC_EVENT: format_int,
@@ -1485,32 +1548,14 @@ the `dict` keys.
1485
1548
  """
1486
1549
 
1487
1550
 
1488
- def create_run_function(
1489
- new_emission_func: Callable[[EmissionMethodTier, dict], dict],
1551
+ def _assign_emissions(
1552
+ cycle_id: str,
1553
+ inventory,
1490
1554
  land_use_change_emission_term_id: str,
1491
1555
  management_change_emission_term_id: str
1492
- ) -> Callable[[str, dict], list[dict]]:
1493
- """
1494
- Create a run function for an emissions from carbon stock change model.
1495
-
1496
- A model-specific `new_emission_func` should be passed as a parameter to this higher-order function to control how
1497
- model ouputs are formatted into HESTIA emission nodes.
1556
+ ):
1498
1557
 
1499
- Parameters
1500
- ----------
1501
- new_emission_func : Callable[[EmissionMethodTier, tuple], dict]
1502
- A function, with the signature `(method_tier: dict, **kwargs: dict) -> (emission_node: dict)`.
1503
- land_use_change_emission_term_id : str
1504
- The term id for emissions allocated to land use changes.
1505
- management_change_emission_term_id : str
1506
- The term id for emissions allocated to management changes.
1507
-
1508
- Returns
1509
- -------
1510
- Callable[[str, dict], list[dict]]
1511
- The customised `run` function with the signature `(cycle_id: str, inventory: dict) -> emissions: list[dict]`.
1512
- """
1513
- def reduce_emissions(result: dict, year: int, cycle_id: str, inventory: dict):
1558
+ def assign(result: dict, year: int, cycle_id: str, inventory: dict):
1514
1559
  """
1515
1560
  Assign emissions to either the land use or management change term ids and sum together.
1516
1561
  """
@@ -1560,7 +1605,36 @@ def create_run_function(
1560
1605
 
1561
1606
  return result | emission_dict | zero_emission_dict
1562
1607
 
1563
- def run(cycle_id: str, inventory: dict) -> list[dict]:
1608
+ def should_run_year(year: int) -> bool:
1609
+ return cycle_id in inventory.get(year, {}).get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
1610
+
1611
+ return reduce(
1612
+ lambda result, year: assign(result, year, cycle_id, inventory),
1613
+ (year for year in inventory.keys() if should_run_year(year)),
1614
+ {}
1615
+ )
1616
+
1617
+
1618
+ def create_run_function(
1619
+ new_emission_func: Callable[[EmissionMethodTier, dict], dict]
1620
+ ) -> Callable[[str, dict], list[dict]]:
1621
+ """
1622
+ Create a run function for an emissions from carbon stock change model.
1623
+
1624
+ A model-specific `new_emission_func` should be passed as a parameter to this higher-order function to control how
1625
+ model ouputs are formatted into HESTIA emission nodes.
1626
+
1627
+ Parameters
1628
+ ----------
1629
+ new_emission_func : Callable[[EmissionMethodTier, tuple], dict]
1630
+ A function, with the signature `(method_tier: dict, **kwargs: dict) -> (emission_node: dict)`.
1631
+
1632
+ Returns
1633
+ -------
1634
+ Callable[[str, dict], list[dict]]
1635
+ The customised `run` function with the signature `(cycle_id: str, inventory: dict) -> emissions: list[dict]`.
1636
+ """
1637
+ def run(assigned_emissions: dict) -> list[dict]:
1564
1638
  """
1565
1639
  Calculate emissions for a specific cycle using from a carbon stock change using pre-compiled inventory data.
1566
1640
 
@@ -1579,27 +1653,17 @@ def create_run_function(
1579
1653
  list[dict]
1580
1654
  A list of [Emission](https://www.hestia.earth/schema/Emission) nodes containing model results.
1581
1655
  """
1582
-
1583
- def should_run_year(year: int) -> bool:
1584
- return cycle_id in inventory.get(year, {}).get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
1585
-
1586
- assigned_emissions = reduce(
1587
- lambda result, year: reduce_emissions(result, year, cycle_id, inventory),
1588
- (year for year in inventory.keys() if should_run_year(year)),
1589
- {}
1590
- )
1591
-
1592
1656
  return [
1593
1657
  new_emission_func(
1594
1658
  term_id=emission_term_id,
1595
- method_tier=_get_emission_method(total_emission),
1659
+ method_tier=_get_emission_method(stock_change_emission),
1596
1660
  **calc_descriptive_stats(
1597
- total_emission.value,
1661
+ stock_change_emission.value,
1598
1662
  EmissionStatsDefinition.SIMULATED,
1599
1663
  decimals=6
1600
1664
  )
1601
- ) for emission_term_id, total_emission in assigned_emissions.items()
1602
- if isinstance(total_emission, CarbonStockChangeEmission)
1665
+ ) for emission_term_id, stock_change_emission in assigned_emissions.items()
1666
+ if isinstance(stock_change_emission, CarbonStockChangeEmission)
1603
1667
  ]
1604
1668
 
1605
1669
  return run
@@ -1616,7 +1680,7 @@ def get_zero_emission(year):
1616
1680
 
1617
1681
  def _get_emission_method(emission: CarbonStockChangeEmission):
1618
1682
  method = emission.method
1619
- return method if isinstance(method, EmissionMethodTier) else EmissionMethodTier.TIER_1
1683
+ return method if isinstance(method, EmissionMethodTier) else _DEFAULT_EMISSION_METHOD_TIER
1620
1684
 
1621
1685
 
1622
1686
  def is_soil_based_system(cycles, site_type):
@@ -1,6 +1,5 @@
1
1
  from hestia_earth.schema import EmissionMethodTier
2
2
 
3
- from hestia_earth.models.log import logRequirements, logShouldRun
4
3
  from hestia_earth.models.utils.emission import _new_emission
5
4
 
6
5
  from .organicCarbonPerHa_tier_1 import _assign_ipcc_land_use_category, get_valid_management_nodes
@@ -43,8 +42,10 @@ RETURNS = {
43
42
  }
44
43
  TERM_ID = 'co2ToAirSoilOrganicCarbonStockChangeLandUseChange,co2ToAirSoilOrganicCarbonStockChangeManagementChange'
45
44
 
46
- _LU_EMISSION_TERM_ID = "co2ToAirSoilOrganicCarbonStockChangeLandUseChange"
47
- _MG_EMISSION_TERM_ID = "co2ToAirSoilOrganicCarbonStockChangeManagementChange"
45
+ _TERM_IDS = TERM_ID.split(",")
46
+
47
+ _LU_EMISSION_TERM_ID = _TERM_IDS[0]
48
+ _MG_EMISSION_TERM_ID = _TERM_IDS[1]
48
49
 
49
50
  _DEPTH_UPPER = 0
50
51
  _DEPTH_LOWER = 30
@@ -115,24 +116,18 @@ def run(cycle: dict) -> list[dict]:
115
116
  """
116
117
  should_run_exec = create_should_run_function(
117
118
  _CARBON_STOCK_TERM_ID,
119
+ _LU_EMISSION_TERM_ID,
120
+ _MG_EMISSION_TERM_ID,
118
121
  depth_upper=_DEPTH_UPPER,
119
122
  depth_lower=_DEPTH_LOWER,
120
- measurements_mandatory=False, # Model can allocate zero emissions to LUC with enough landCover data
123
+ measurements_required=False, # Model can allocate zero emissions to LUC with enough landCover data
121
124
  get_valid_management_nodes_func=get_valid_management_nodes,
122
125
  summarise_land_use_func=lambda nodes: _assign_ipcc_land_use_category(nodes, None),
123
126
  detect_land_use_change_func=lambda a, b: a != b
124
127
  )
125
128
 
126
- run_exec = create_run_function(
127
- new_emission_func=_emission,
128
- land_use_change_emission_term_id=_LU_EMISSION_TERM_ID,
129
- management_change_emission_term_id=_MG_EMISSION_TERM_ID
130
- )
131
-
132
- should_run, cycle_id, inventory, logs = should_run_exec(cycle)
129
+ run_exec = create_run_function(new_emission_func=_emission)
133
130
 
134
- for term_id in [_LU_EMISSION_TERM_ID, _MG_EMISSION_TERM_ID]:
135
- logRequirements(cycle, model=MODEL, term=term_id, **logs)
136
- logShouldRun(cycle, MODEL, term_id, should_run)
131
+ should_run, assigned_emissions = should_run_exec(cycle)
137
132
 
138
- return run_exec(cycle_id, inventory) if should_run else []
133
+ return run_exec(assigned_emissions) if should_run else []
@@ -811,7 +811,7 @@ def _log_emission_data(should_run: bool, term_id: _EmissionTermId, cycle: dict,
811
811
  formatted_inventory = _format_inventory(term_id, cycle.get("@id"), inventory)
812
812
 
813
813
  logRequirements(cycle, model=MODEL, term=term_id, **formatted_logs, inventory=formatted_inventory)
814
- logShouldRun(cycle, MODEL, term_id, should_run)
814
+ logShouldRun(cycle, MODEL, term_id, should_run, methodTier=TIER)
815
815
 
816
816
 
817
817
  def _should_run(cycle: dict):
@@ -178,3 +178,7 @@ def format_decimal_percentage(
178
178
  def format_enum(value: Optional[Enum], default: str = "None") -> str:
179
179
  """Format an enum for logging in a table."""
180
180
  return format_str(value.value) if isinstance(value, Enum) else default
181
+
182
+
183
+ def format_conditional_message(value: bool, on_true: str = "True", on_false: str = "False") -> str:
184
+ return format_str(on_true if bool(value) else on_false)