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

Files changed (28) hide show
  1. hestia_earth/models/config/Site.json +11 -3
  2. hestia_earth/models/emepEea2019/fuelCombustion_utils.py +21 -21
  3. hestia_earth/models/hestia/landOccupationDuringCycle.py +9 -27
  4. hestia_earth/models/hestia/soilClassification.py +314 -0
  5. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +5 -15
  6. hestia_earth/models/ipcc2019/belowGroundBiomass.py +5 -15
  7. hestia_earth/models/ipcc2019/biocharOrganicCarbonPerHa.py +5 -39
  8. hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +5 -5
  9. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +6 -21
  10. hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +4 -5
  11. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +5 -5
  12. hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +17 -46
  13. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +10 -10
  14. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +4 -19
  15. hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +0 -9
  16. hestia_earth/models/log.py +71 -1
  17. hestia_earth/models/mocking/search-results.json +1 -1
  18. hestia_earth/models/utils/blank_node.py +12 -4
  19. hestia_earth/models/version.py +1 -1
  20. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.15.dist-info}/METADATA +2 -2
  21. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.15.dist-info}/RECORD +28 -25
  22. tests/models/ecoalimV9/test_cycle.py +2 -2
  23. tests/models/hestia/test_soilClassification.py +72 -0
  24. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +4 -48
  25. tests/models/test_log.py +128 -0
  26. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.15.dist-info}/LICENSE +0 -0
  27. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.15.dist-info}/WHEEL +0 -0
  28. {hestia_earth_models-0.74.14.dist-info → hestia_earth_models-0.74.15.dist-info}/top_level.txt +0 -0
@@ -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, format_nd_array, format_number,
19
- get_ditch_frac, get_emission_factor, OrganicSoilCategory, remap_categories, valid_eco_climate_zone
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=format_number(land_occupation),
230
- histosol=format_number(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: _format_number(group.get(KEY, {}).get(id, 0)) for id in unique_cycles
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
- _format_number(mean(value.value))
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: _format_bool,
1494
- _InventoryKey.YEARS_SINCE_LUC_EVENT: _format_int,
1495
- _InventoryKey.YEARS_SINCE_INVENTORY_START: _format_int
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, 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,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
  """
@@ -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
@@ -8,7 +8,7 @@ from hestia_earth.utils.stats import calc_z_critical
8
8
  from hestia_earth.utils.stats import (
9
9
  repeat_single, truncated_normal_1d
10
10
  )
11
- from hestia_earth.models.log import log_as_table
11
+ from hestia_earth.models.log import format_bool, format_enum, format_float, log_as_table
12
12
  from hestia_earth.models.utils.blank_node import cumulative_nodes_term_match, node_term_match
13
13
  from hestia_earth.models.utils.term import get_cover_crop_property_terms, get_irrigated_terms
14
14
 
@@ -268,21 +268,6 @@ def sample_constant(*, iterations: int, value: float, **_) -> NDArray:
268
268
  return repeat_single(shape=(1, iterations), value=value)
269
269
 
270
270
 
271
- def format_bool(value: Optional[bool]) -> str:
272
- """Format a bool for logging in a table."""
273
- return str(bool(value))
274
-
275
-
276
- def format_number(value: Optional[float]) -> str:
277
- """Format a float for logging in a table."""
278
- return f"{value:.1f}" if isinstance(value, (float, int)) else "None"
279
-
280
-
281
- def format_enum(value: Optional[Enum]) -> str:
282
- """Format an enum for logging in a table."""
283
- return value.value if isinstance(value, Enum) else "None"
284
-
285
-
286
271
  def format_bool_list(values: Optional[list[bool]]) -> str:
287
272
  """Format a list of bools for logging in a table."""
288
273
  return (
@@ -291,10 +276,10 @@ def format_bool_list(values: Optional[list[bool]]) -> str:
291
276
  )
292
277
 
293
278
 
294
- def format_number_list(values: Optional[list[float]]) -> str:
279
+ def format_float_list(values: Optional[list[float]]) -> str:
295
280
  """Format a list of floats for logging in a table."""
296
281
  return (
297
- " ".join(format_number(value) for value in values) or "None"if isinstance(values, list)
282
+ " ".join(format_float(value, ndigits=1) for value in values) or "None"if isinstance(values, list)
298
283
  else "None"
299
284
  )
300
285
 
@@ -303,7 +288,7 @@ def format_soil_inventory(inventory: list[SoilData]) -> str:
303
288
  return log_as_table(
304
289
  {
305
290
  "term-id": data.term_id,
306
- "value": format_number(data.value),
291
+ "value": format_float(data.value),
307
292
  "category": format_enum(data.category)
308
293
  } for data in inventory
309
294
  ) if inventory else "None"
@@ -1,6 +1,5 @@
1
1
  from enum import Enum
2
2
  from typing import Literal
3
- import numpy as np
4
3
 
5
4
  from hestia_earth.schema import SiteSiteType
6
5
  from hestia_earth.utils.model import find_primary_product
@@ -157,11 +156,3 @@ def valid_eco_climate_zone(
157
156
  Validate that the model should run for a specific eco-climate zone.
158
157
  """
159
158
  return isinstance(eco_climate_zone, EcoClimateZone) and eco_climate_zone not in _EXCLUDED_ECO_CLIMATE_ZONES
160
-
161
-
162
- def format_number(value) -> str:
163
- return f"{value:.3g}" if isinstance(value, (float, int)) else "None"
164
-
165
-
166
- def format_nd_array(value) -> str:
167
- return f"{np.mean(value):.3g} ± {np.std(value):.3g}" if isinstance(value, np.ndarray) else "None"
@@ -1,7 +1,11 @@
1
+ from enum import Enum
2
+ from functools import reduce
1
3
  import os
2
4
  import sys
3
5
  import logging
4
- from typing import Union, List
6
+ from typing import List, Optional, Union
7
+ from numpy.typing import NDArray
8
+ from numpy import ndarray
5
9
 
6
10
  LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
7
11
  _EXTENDED_LOGS = os.getenv('LOG_EXTENDED', 'true') == 'true'
@@ -108,3 +112,69 @@ def log_blank_nodes_id(blank_nodes: List[dict]):
108
112
  List of blank nodes, like Product, Input, Measurement, etc.
109
113
  """
110
114
  return ';'.join([p.get('term', {}).get('@id') for p in blank_nodes if p.get('term', {}).get('@id')]) or 'None'
115
+
116
+
117
+ _INVALID_CHARS = {"_", ":", ",", "="}
118
+ _REPLACEMENT_CHAR = "-"
119
+
120
+
121
+ def format_str(value: Optional[str], default: str = "None") -> str:
122
+ """Format a string for logging in a table. Remove all characters used to render the table on the front end."""
123
+ return (
124
+ reduce(lambda x, char: x.replace(char, _REPLACEMENT_CHAR), _INVALID_CHARS, str(value))
125
+ if value else default
126
+ )
127
+
128
+
129
+ def format_bool(value: Optional[bool], default: str = "None") -> str:
130
+ return str(value) if isinstance(value, bool) else default
131
+
132
+
133
+ def format_float(value: Union[int, float, None], unit: str = "", default: str = "None", ndigits: int = 3) -> str:
134
+ return " ".join(
135
+ string for string in [f"{round(value, ndigits)}", format_str(unit, "")] if string
136
+ ) if isinstance(value, (float, int)) else default
137
+
138
+
139
+ def format_int(value: Union[int, float, None], unit: str = "", default: str = "None") -> str:
140
+ return format_float(value, unit=unit, default=default, ndigits=None)
141
+
142
+
143
+ def _format_nd_array(value: Optional[NDArray], unit: str = "", default: str = "None", ndigits: int = 3) -> str:
144
+ return " ".join(
145
+ string for string in [
146
+ f"{format_float(value.mean(), ndigits=ndigits)} ± {format_float(value.std(), ndigits=ndigits)}",
147
+ format_str(unit, "")
148
+ ] if string
149
+ ) if isinstance(value, ndarray) else default
150
+
151
+
152
+ TYPE_TO_FORMAT_FUNC = {
153
+ ndarray: _format_nd_array,
154
+ (float, int): format_float
155
+ }
156
+
157
+
158
+ def format_nd_array(value: Optional[NDArray], unit: str = "", default: str = "None", ndigits: int = 3) -> str:
159
+ """
160
+ Format a numpy array for logging in a table.
161
+
162
+ Values that are floats and ints are logged using `format_float`.
163
+ """
164
+ format_func = next(
165
+ (func for type_, func in TYPE_TO_FORMAT_FUNC.items() if isinstance(value, type_)),
166
+ None
167
+ )
168
+ return format_func(value, unit=unit, default=default, ndigits=ndigits) if format_func else default
169
+
170
+
171
+ def format_decimal_percentage(
172
+ value: Optional[float], unit: str = "pct", default: str = "None", ndigits: int = 3
173
+ ) -> str:
174
+ """Format a decimal percentage (0-1) as a percentage (0-100%) for logging in a table."""
175
+ return format_float(value * 100, unit=unit, ndigits=ndigits) if isinstance(value, (float, int)) else default
176
+
177
+
178
+ def format_enum(value: Optional[Enum], default: str = "None") -> str:
179
+ """Format an enum for logging in a table."""
180
+ return format_str(value.value) if isinstance(value, Enum) else default