hestia-earth-models 0.74.14__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.

Files changed (37) hide show
  1. hestia_earth/models/cache_nodes.py +9 -6
  2. hestia_earth/models/config/ImpactAssessment.json +0 -22
  3. hestia_earth/models/config/Site.json +11 -3
  4. hestia_earth/models/cycle/completeness/material.py +2 -3
  5. hestia_earth/models/emepEea2019/fuelCombustion_utils.py +21 -21
  6. hestia_earth/models/hestia/landOccupationDuringCycle.py +9 -27
  7. hestia_earth/models/hestia/resourceUse_utils.py +49 -20
  8. hestia_earth/models/hestia/soilClassification.py +314 -0
  9. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +5 -15
  10. hestia_earth/models/ipcc2019/belowGroundBiomass.py +5 -15
  11. hestia_earth/models/ipcc2019/biocharOrganicCarbonPerHa.py +5 -39
  12. hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +5 -5
  13. hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChange.py +10 -15
  14. hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChange.py +11 -16
  15. hestia_earth/models/ipcc2019/co2ToAirBiocharStockChange.py +7 -17
  16. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +123 -74
  17. hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +4 -5
  18. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChange.py +10 -15
  19. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +5 -5
  20. hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +18 -47
  21. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +10 -10
  22. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +4 -19
  23. hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +0 -9
  24. hestia_earth/models/log.py +75 -1
  25. hestia_earth/models/mocking/search-results.json +1 -1
  26. hestia_earth/models/utils/blank_node.py +12 -4
  27. hestia_earth/models/version.py +1 -1
  28. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.16.dist-info}/METADATA +15 -7
  29. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.16.dist-info}/RECORD +37 -34
  30. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.16.dist-info}/WHEEL +1 -1
  31. tests/models/ecoalimV9/test_cycle.py +2 -2
  32. tests/models/hestia/test_landTransformation20YearAverageDuringCycle.py +4 -8
  33. tests/models/hestia/test_soilClassification.py +72 -0
  34. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +4 -48
  35. tests/models/test_log.py +128 -0
  36. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.16.dist-info/licenses}/LICENSE +0 -0
  37. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.16.dist-info}/top_level.txt +0 -0
@@ -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 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_number(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"
@@ -1450,21 +1509,6 @@ def _format_land_use_inventory(land_use_inventory: dict) -> str:
1450
1509
  ) if should_run else "None"
1451
1510
 
1452
1511
 
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
1512
  def _format_column_header(method: MeasurementMethodClassification, inventory_key: _InventoryKey) -> str:
1469
1513
  """
1470
1514
  Format a measurement method classification and inventory key for logging in a table as a column header. Replace any
@@ -1483,16 +1527,20 @@ def _format_named_tuple(value: Optional[Union[CarbonStock, CarbonStockChange, Ca
1483
1527
  Extract and format just the value and discard the other data. If the value is invalid, return `"None"` as a string.
1484
1528
  """
1485
1529
  return (
1486
- _format_number(mean(value.value))
1530
+ format_nd_array(mean(value.value))
1487
1531
  if isinstance(value, (CarbonStock, CarbonStockChange, CarbonStockChangeEmission))
1488
1532
  else "None"
1489
1533
  )
1490
1534
 
1491
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
+
1492
1540
  _LAND_USE_INVENTORY_KEY_TO_FORMAT_FUNC = {
1493
- _InventoryKey.LAND_USE_CHANGE_EVENT: _format_bool,
1494
- _InventoryKey.YEARS_SINCE_LUC_EVENT: _format_int,
1495
- _InventoryKey.YEARS_SINCE_INVENTORY_START: _format_int
1541
+ _InventoryKey.LAND_USE_CHANGE_EVENT: format_bool,
1542
+ _InventoryKey.YEARS_SINCE_LUC_EVENT: format_int,
1543
+ _InventoryKey.YEARS_SINCE_INVENTORY_START: format_int
1496
1544
  }
1497
1545
  """
1498
1546
  Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of
@@ -1500,32 +1548,14 @@ the `dict` keys.
1500
1548
  """
1501
1549
 
1502
1550
 
1503
- def create_run_function(
1504
- new_emission_func: Callable[[EmissionMethodTier, dict], dict],
1551
+ def _assign_emissions(
1552
+ cycle_id: str,
1553
+ inventory,
1505
1554
  land_use_change_emission_term_id: str,
1506
1555
  management_change_emission_term_id: str
1507
- ) -> Callable[[str, dict], list[dict]]:
1508
- """
1509
- Create a run function for an emissions from carbon stock change model.
1556
+ ):
1510
1557
 
1511
- A model-specific `new_emission_func` should be passed as a parameter to this higher-order function to control how
1512
- model ouputs are formatted into HESTIA emission nodes.
1513
-
1514
- Parameters
1515
- ----------
1516
- new_emission_func : Callable[[EmissionMethodTier, tuple], dict]
1517
- A function, with the signature `(method_tier: dict, **kwargs: dict) -> (emission_node: dict)`.
1518
- land_use_change_emission_term_id : str
1519
- The term id for emissions allocated to land use changes.
1520
- management_change_emission_term_id : str
1521
- The term id for emissions allocated to management changes.
1522
-
1523
- Returns
1524
- -------
1525
- Callable[[str, dict], list[dict]]
1526
- The customised `run` function with the signature `(cycle_id: str, inventory: dict) -> emissions: list[dict]`.
1527
- """
1528
- def reduce_emissions(result: dict, year: int, cycle_id: str, inventory: dict):
1558
+ def assign(result: dict, year: int, cycle_id: str, inventory: dict):
1529
1559
  """
1530
1560
  Assign emissions to either the land use or management change term ids and sum together.
1531
1561
  """
@@ -1575,7 +1605,36 @@ def create_run_function(
1575
1605
 
1576
1606
  return result | emission_dict | zero_emission_dict
1577
1607
 
1578
- 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]:
1579
1638
  """
1580
1639
  Calculate emissions for a specific cycle using from a carbon stock change using pre-compiled inventory data.
1581
1640
 
@@ -1594,27 +1653,17 @@ def create_run_function(
1594
1653
  list[dict]
1595
1654
  A list of [Emission](https://www.hestia.earth/schema/Emission) nodes containing model results.
1596
1655
  """
1597
-
1598
- def should_run_year(year: int) -> bool:
1599
- return cycle_id in inventory.get(year, {}).get(_InventoryKey.SHARE_OF_EMISSION, {}).keys()
1600
-
1601
- assigned_emissions = reduce(
1602
- lambda result, year: reduce_emissions(result, year, cycle_id, inventory),
1603
- (year for year in inventory.keys() if should_run_year(year)),
1604
- {}
1605
- )
1606
-
1607
1656
  return [
1608
1657
  new_emission_func(
1609
1658
  term_id=emission_term_id,
1610
- method_tier=_get_emission_method(total_emission),
1659
+ method_tier=_get_emission_method(stock_change_emission),
1611
1660
  **calc_descriptive_stats(
1612
- total_emission.value,
1661
+ stock_change_emission.value,
1613
1662
  EmissionStatsDefinition.SIMULATED,
1614
1663
  decimals=6
1615
1664
  )
1616
- ) for emission_term_id, total_emission in assigned_emissions.items()
1617
- if isinstance(total_emission, CarbonStockChangeEmission)
1665
+ ) for emission_term_id, stock_change_emission in assigned_emissions.items()
1666
+ if isinstance(stock_change_emission, CarbonStockChangeEmission)
1618
1667
  ]
1619
1668
 
1620
1669
  return run
@@ -1631,7 +1680,7 @@ def get_zero_emission(year):
1631
1680
 
1632
1681
  def _get_emission_method(emission: CarbonStockChangeEmission):
1633
1682
  method = emission.method
1634
- return method if isinstance(method, EmissionMethodTier) else EmissionMethodTier.TIER_1
1683
+ return method if isinstance(method, EmissionMethodTier) else _DEFAULT_EMISSION_METHOD_TIER
1635
1684
 
1636
1685
 
1637
1686
  def is_soil_based_system(cycles, site_type):
@@ -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, format_nd_array, format_number, get_emission_factor,
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=format_number(land_occupation),
187
- histosol=format_number(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,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 []
@@ -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, format_number, get_emission_factor, OrganicSoilCategory,
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"{format_number(emission_factor_mean)} ± {format_number(emission_factor_sd)}",
126
- land_occupation=format_number(land_occupation),
127
- histosol=format_number(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([
@@ -10,7 +10,10 @@ from hestia_earth.utils.tools import safe_parse_float
10
10
  from hestia_earth.utils.stats import gen_seed, repeat_single, truncated_normal_1d
11
11
  from hestia_earth.utils.descriptive_stats import calc_descriptive_stats
12
12
 
13
- from hestia_earth.models.log import debugMissingLookup, log_as_table, logRequirements, logShouldRun
13
+ from hestia_earth.models.log import (
14
+ debugMissingLookup, format_bool, format_decimal_percentage, format_float, format_nd_array, format_str, log_as_table,
15
+ logRequirements, logShouldRun
16
+ )
14
17
  from hestia_earth.models.utils.blank_node import group_nodes_by_year
15
18
  from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
16
19
  from hestia_earth.models.utils.emission import _new_emission
@@ -670,60 +673,28 @@ def _compile_inventory(
670
673
  return should_run, inventory, logs
671
674
 
672
675
 
673
- def _format_bool(value: Optional[bool]) -> str:
674
- """Format a bool for logging in a table."""
675
- return str(bool(value))
676
-
677
-
678
- def _format_number(value: Optional[float], unit: Optional[str] = None) -> str:
679
- """Format a float for logging in a table."""
680
- return f"{value:.1f}{f' {unit}' if unit else ''}" if isinstance(value, (float, int)) else "None"
681
-
682
-
683
- def _format_nd_array(value: Optional[npt.NDArray], unit: Optional[str] = None) -> str:
684
- """Format a numpy array for logging in a table."""
685
- return (
686
- f"{_format_number(value.mean())} ± {_format_number(value.std())}" + f"{f' {unit}' if unit else ''}"
687
- if isinstance(value, np.ndarray) else "None"
688
- )
689
-
690
-
691
- def _format_decimal_percentage(value: Optional[float], unit: Optional[str] = "pct") -> str:
692
- """Format a decimal percentage (0-1) as a percentage (0-100%) for logging in a table."""
693
- return _format_number(value * 100, unit) if isinstance(value, (float, int)) else "None"
694
-
695
-
696
- _INVALID_CHARS = {"_", ":", ",", "="}
697
- _REPLACEMENT_CHAR = "-"
698
-
699
-
700
- def _format_str(value: str, *_) -> str:
701
- """Format a string for logging in a table. Remove all characters used to render the table on the front end."""
702
- return reduce(lambda x, char: x.replace(char, _REPLACEMENT_CHAR), _INVALID_CHARS, str(value))
703
-
704
-
705
676
  def _format_column_header(*keys: tuple[Union[Enum, str], ...]) -> str:
706
677
  """Format a variable number of enums and strings for logging as a table column header."""
707
- return " ".join(_format_str(k.value if isinstance(k, Enum) else str(k)) for k in keys)
678
+ return " ".join(format_str(k.value if isinstance(k, Enum) else format_str(k)) for k in keys)
708
679
 
709
680
 
710
681
  def _format_eco_climate_zone(value: EcoClimateZone) -> str:
711
682
  """Format an eco-climate zone for logging."""
712
683
  return (
713
- _format_str(str(value.name).lower().replace("_", " ").capitalize()) if isinstance(value, EcoClimateZone)
684
+ format_str(str(value.name).lower().replace("_", " ").capitalize()) if isinstance(value, EcoClimateZone)
714
685
  else "None"
715
686
  )
716
687
 
717
688
 
718
689
  _LOGS_FORMAT_DATA: dict[str, Callable] = {
719
- "has_valid_site_type": _format_bool,
690
+ "has_valid_site_type": format_bool,
720
691
  "eco_climate_zone": _format_eco_climate_zone,
721
- "has_valid_eco_climate_zone": _format_bool,
722
- "has_land_cover_nodes": _format_bool,
723
- "should_compile_inventory": _format_bool,
724
- "percent_burned": lambda x: _format_number(x, "pct"),
692
+ "has_valid_eco_climate_zone": format_bool,
693
+ "has_land_cover_nodes": format_bool,
694
+ "should_compile_inventory": format_bool,
695
+ "percent_burned": lambda x: format_float(x, "pct"),
725
696
  }
726
- _DEFAULT_FORMAT_FUNC = _format_str
697
+ _DEFAULT_FORMAT_FUNC = format_str
727
698
 
728
699
 
729
700
  def _format_logs(logs: dict) -> dict[str, str]:
@@ -735,23 +706,23 @@ def _format_logs(logs: dict) -> dict[str, str]:
735
706
 
736
707
  _INVENTORY_FORMAT_DATA: dict[_InventoryKey, dict[Literal["filter_by", "format_func"], Any]] = {
737
708
  "fuel_burnt_per_category": {
738
- "format_func": lambda x: _format_nd_array(x, "kg")
709
+ "format_func": lambda x: format_nd_array(x, "kg")
739
710
  },
740
711
  "annual_emissions": {
741
712
  "filter_by": ("term_id", ),
742
- "format_func": lambda x: _format_nd_array(x, "kg")
713
+ "format_func": lambda x: format_nd_array(x, "kg")
743
714
  },
744
715
  "amortised_emissions": {
745
716
  "filter_by": ("term_id", ),
746
- "format_func": lambda x: _format_nd_array(x, "kg")
717
+ "format_func": lambda x: format_nd_array(x, "kg")
747
718
  },
748
719
  "share_of_emissions": {
749
720
  "filter_by": ("cycle_id", ),
750
- "format_func": _format_decimal_percentage
721
+ "format_func": format_decimal_percentage
751
722
  },
752
723
  "allocated_emissions": {
753
724
  "filter_by": ("term_id", "cycle_id"),
754
- "format_func": lambda x: _format_nd_array(x, "kg")
725
+ "format_func": lambda x: format_nd_array(x, "kg")
755
726
  }
756
727
  }
757
728
  """
@@ -840,7 +811,7 @@ def _log_emission_data(should_run: bool, term_id: _EmissionTermId, cycle: dict,
840
811
  formatted_inventory = _format_inventory(term_id, cycle.get("@id"), inventory)
841
812
 
842
813
  logRequirements(cycle, model=MODEL, term=term_id, **formatted_logs, inventory=formatted_inventory)
843
- logShouldRun(cycle, MODEL, term_id, should_run)
814
+ logShouldRun(cycle, MODEL, term_id, should_run, methodTier=TIER)
844
815
 
845
816
 
846
817
  def _should_run(cycle: dict):
@@ -2,9 +2,9 @@ from functools import reduce
2
2
  from pydash.objects import merge
3
3
  from types import ModuleType
4
4
 
5
- from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
5
+ from hestia_earth.models.log import format_bool, format_enum, format_float, log_as_table, logRequirements, logShouldRun
6
6
 
7
- from .organicCarbonPerHa_utils import format_bool, format_bool_list, format_enum, format_number, format_number_list
7
+ from .organicCarbonPerHa_utils import format_bool_list, format_float_list
8
8
  from . import organicCarbonPerHa_tier_1 as tier_1
9
9
  from . import organicCarbonPerHa_tier_2 as tier_2
10
10
  from . import MODEL # noqa
@@ -204,20 +204,20 @@ def _get_unique_inventory_keys(inventory: dict) -> list:
204
204
 
205
205
  _INVENTORY_KEY_TO_FORMAT_FUNC = {
206
206
  tier_2._InventoryKey.SHOULD_RUN: format_bool,
207
- tier_2._InventoryKey.TEMP_MONTHLY: format_number_list,
208
- tier_2._InventoryKey.PRECIP_MONTHLY: format_number_list,
209
- tier_2._InventoryKey.PET_MONTHLY: format_number_list,
207
+ tier_2._InventoryKey.TEMP_MONTHLY: format_float_list,
208
+ tier_2._InventoryKey.PRECIP_MONTHLY: format_float_list,
209
+ tier_2._InventoryKey.PET_MONTHLY: format_float_list,
210
210
  tier_2._InventoryKey.IRRIGATED_MONTHLY: format_bool_list,
211
- tier_2._InventoryKey.SAND_CONTENT: format_number,
212
- tier_2._InventoryKey.CARBON_INPUT: format_number,
213
- tier_2._InventoryKey.N_CONTENT: format_number,
214
- tier_2._InventoryKey.LIGNIN_CONTENT: format_number,
211
+ tier_2._InventoryKey.SAND_CONTENT: format_float,
212
+ tier_2._InventoryKey.CARBON_INPUT: format_float,
213
+ tier_2._InventoryKey.N_CONTENT: format_float,
214
+ tier_2._InventoryKey.LIGNIN_CONTENT: format_float,
215
215
  tier_2._InventoryKey.TILLAGE_CATEGORY: format_enum,
216
216
  tier_2._InventoryKey.IS_PADDY_RICE: format_bool,
217
217
  tier_1._InventoryKey.SHOULD_RUN: format_bool,
218
218
  tier_1._InventoryKey.LU_CATEGORY: format_enum,
219
219
  tier_1._InventoryKey.MG_CATEGORY: format_enum,
220
- tier_1._InventoryKey.CI_CATEGORY: format_enum,
220
+ tier_1._InventoryKey.CI_CATEGORY: format_enum
221
221
  }
222
222
  """
223
223
  Map inventory keys to format functions. The columns in inventory logged as a table will also be sorted in the order of