hestia-earth-models 0.70.1__py3-none-any.whl → 0.70.2__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.
Files changed (42) hide show
  1. hestia_earth/models/cml2001Baseline/resourceUseMineralsAndMetalsDuringCycle.py +2 -1
  2. hestia_earth/models/config/Cycle.json +60 -0
  3. hestia_earth/models/config/Site.json +8 -0
  4. hestia_earth/models/hestia/excretaKgMass.py +1 -1
  5. hestia_earth/models/hestia/management.py +4 -6
  6. hestia_earth/models/hestia/pToSurfaceWaterAquacultureSystems.py +148 -0
  7. hestia_earth/models/hestia/soilMeasurement.py +1 -1
  8. hestia_earth/models/ipcc2019/ch4ToAirFloodedRice.py +8 -6
  9. hestia_earth/models/ipcc2019/ch4ToAirOrganicSoilCultivation.py +270 -0
  10. hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChange.py +0 -3
  11. hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChange.py +0 -3
  12. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +57 -43
  13. hestia_earth/models/ipcc2019/co2ToAirLimeHydrolysis.py +7 -5
  14. hestia_earth/models/ipcc2019/co2ToAirOrganicSoilCultivation.py +215 -0
  15. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChange.py +0 -3
  16. hestia_earth/models/ipcc2019/n2OToAirOrganicSoilCultivationDirect.py +161 -0
  17. hestia_earth/models/ipcc2019/organicSoilCultivation_utils.py +159 -0
  18. hestia_earth/models/mocking/search-results.json +714 -706
  19. hestia_earth/models/site/grouped_measurement.py +132 -0
  20. hestia_earth/models/utils/__init__.py +4 -3
  21. hestia_earth/models/utils/blank_node.py +38 -9
  22. hestia_earth/models/utils/constant.py +26 -20
  23. hestia_earth/models/utils/product.py +39 -1
  24. hestia_earth/models/utils/property.py +14 -6
  25. hestia_earth/models/version.py +1 -1
  26. {hestia_earth_models-0.70.1.dist-info → hestia_earth_models-0.70.2.dist-info}/METADATA +1 -1
  27. {hestia_earth_models-0.70.1.dist-info → hestia_earth_models-0.70.2.dist-info}/RECORD +42 -31
  28. tests/models/hestia/test_feedConversionRatio.py +2 -3
  29. tests/models/hestia/test_pToSurfaceWaterAquacultureSystems.py +56 -0
  30. tests/models/hestia/test_soilMeasurement.py +11 -19
  31. tests/models/ipcc2019/test_ch4ToAirEntericFermentation.py +2 -5
  32. tests/models/ipcc2019/test_ch4ToAirOrganicSoilCultivation.py +61 -0
  33. tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChange.py +11 -9
  34. tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChange.py +10 -8
  35. tests/models/ipcc2019/test_co2ToAirLimeHydrolysis.py +1 -1
  36. tests/models/ipcc2019/test_co2ToAirOrganicSoilCultivation.py +62 -0
  37. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +11 -9
  38. tests/models/ipcc2019/test_n2OToAirOrganicSoilCultivationDirect.py +61 -0
  39. tests/models/site/test_grouped_measurement.py +20 -0
  40. {hestia_earth_models-0.70.1.dist-info → hestia_earth_models-0.70.2.dist-info}/LICENSE +0 -0
  41. {hestia_earth_models-0.70.1.dist-info → hestia_earth_models-0.70.2.dist-info}/WHEEL +0 -0
  42. {hestia_earth_models-0.70.1.dist-info → hestia_earth_models-0.70.2.dist-info}/top_level.txt +0 -0
@@ -319,7 +319,13 @@ def _add_carbon_stock_change_emissions(
319
319
  value = emission_1.value + emission_2.value
320
320
  start_date = min(emission_1.start_date, emission_2.start_date)
321
321
  end_date = max(emission_1.end_date, emission_2.end_date)
322
- method = min_emission_method_tier(emission_1.method, emission_2.method)
322
+
323
+ methods = [
324
+ method for emission in (emission_1, emission_2)
325
+ if isinstance((method := emission.method), EmissionMethodTier)
326
+ ]
327
+
328
+ method = min_emission_method_tier(*methods) if methods else None
323
329
 
324
330
  return CarbonStockChangeEmission(value, start_date, end_date, method)
325
331
 
@@ -442,7 +448,6 @@ def create_should_run_function(
442
448
 
443
449
  inventory, inventory_logs = (
444
450
  compile_inventory_func(
445
- cycle_id,
446
451
  cycles,
447
452
  carbon_stock_measurements,
448
453
  land_cover_nodes
@@ -464,7 +469,8 @@ def create_should_run_function(
464
469
  logs = should_compile_logs | inventory_logs | {
465
470
  "seed": seed,
466
471
  "has_valid_inventory": has_valid_inventory,
467
- "has_consecutive_years": has_consecutive_years
472
+ "has_consecutive_years": has_consecutive_years,
473
+ "has_stock_measurements": bool(carbon_stock_measurements)
468
474
  }
469
475
 
470
476
  return should_run_, kwargs, logs
@@ -554,7 +560,6 @@ def _create_compile_inventory_function(
554
560
  The `compile_inventory` function.
555
561
  """
556
562
  def compile_inventory(
557
- cycle_id: str,
558
563
  cycles: list[dict],
559
564
  carbon_stock_measurements: list[dict],
560
565
  land_cover_nodes: list[dict]
@@ -1177,20 +1182,19 @@ def _squash_inventory(
1177
1182
  return _InventoryKey.CO2_EMISSION in carbon_stock_inventory.get(method, {}).get(year, {}).keys()
1178
1183
 
1179
1184
  def squash(result: dict, year: int) -> dict:
1180
- update_dict = next(
1181
- (
1182
- {
1183
- year: {
1184
- **_get_land_use_change_data(year, land_use_inventory),
1185
- **reduce(merge, [
1186
- carbon_stock_inventory.get(method, {}).get(year, {}),
1187
- cycle_inventory.get(year, {})
1188
- ], dict())
1189
- }
1190
- } for method in measurement_method_ranking if should_run_group(method, year)
1191
- ),
1192
- {}
1185
+ method = next(
1186
+ (method for method in measurement_method_ranking if should_run_group(method, year)),
1187
+ None
1193
1188
  )
1189
+ update_dict = {
1190
+ year: {
1191
+ **_get_land_use_change_data(year, land_use_inventory),
1192
+ **reduce(merge, [
1193
+ carbon_stock_inventory.get(method, {}).get(year, {}),
1194
+ cycle_inventory.get(year, {})
1195
+ ], dict())
1196
+ }
1197
+ }
1194
1198
  return result | update_dict
1195
1199
 
1196
1200
  return reduce(squash, inventory_years, dict())
@@ -1426,36 +1430,31 @@ def create_run_function(
1426
1430
  Assign emissions to either the land use or management change term ids and sum together.
1427
1431
  """
1428
1432
  data = inventory[year]
1433
+
1429
1434
  years_since_luc_event = data[_InventoryKey.YEARS_SINCE_LUC_EVENT]
1430
1435
  years_since_inventory_start = data[_InventoryKey.YEARS_SINCE_INVENTORY_START]
1436
+ share_of_emission = data[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
1437
+
1438
+ co2_emission = data.get(_InventoryKey.CO2_EMISSION)
1431
1439
 
1440
+ has_co2_emission = bool(co2_emission)
1432
1441
  is_luc_emission = bool(years_since_luc_event) and years_since_luc_event <= _TRANSITION_PERIOD_YEARS
1433
1442
  is_data_complete = bool(years_since_inventory_start) and years_since_inventory_start >= _TRANSITION_PERIOD_YEARS
1434
1443
 
1435
- if is_luc_emission:
1436
- # If LUC emission allocate emissions to land use change AND add corresponding zero emission to management
1437
- emission_term_id = land_use_change_emission_term_id
1438
- zero_emission_term_id = management_change_emission_term_id
1439
- elif is_data_complete:
1440
- # If management emission && data complete allocate emissions to management change AND add corresponding
1441
- # zero emission to management
1442
- emission_term_id = management_change_emission_term_id
1443
- zero_emission_term_id = land_use_change_emission_term_id
1444
- else:
1445
- # If management emission, but not data complete allocate emissions to management change only
1446
- emission_term_id = management_change_emission_term_id
1447
- zero_emission_term_id = None
1444
+ emission_term_id = (
1445
+ land_use_change_emission_term_id if is_luc_emission else management_change_emission_term_id
1446
+ ) if has_co2_emission else None
1448
1447
 
1449
- rescaled_emission = _rescale_carbon_stock_change_emission(
1450
- data[_InventoryKey.CO2_EMISSION], data[_InventoryKey.SHARE_OF_EMISSION][cycle_id]
1448
+ zero_emission_term_id = (
1449
+ management_change_emission_term_id if is_luc_emission else
1450
+ (land_use_change_emission_term_id if is_data_complete else None)
1451
1451
  )
1452
1452
 
1453
- zero_emission = CarbonStockChangeEmission(
1454
- value=array(0),
1455
- start_date=rescaled_emission.start_date,
1456
- end_date=rescaled_emission.end_date,
1457
- method=rescaled_emission.method
1458
- ) if zero_emission_term_id else None
1453
+ rescaled_emission = _rescale_carbon_stock_change_emission(
1454
+ co2_emission, share_of_emission
1455
+ ) if emission_term_id else None
1456
+
1457
+ zero_emission = get_zero_emission(year) if zero_emission_term_id else None
1459
1458
 
1460
1459
  previous_emission = result.get(emission_term_id)
1461
1460
  previous_zero_emission = result.get(zero_emission_term_id)
@@ -1465,7 +1464,7 @@ def create_run_function(
1465
1464
  _add_carbon_stock_change_emissions(previous_emission, rescaled_emission) if previous_emission
1466
1465
  else rescaled_emission
1467
1466
  )
1468
- }
1467
+ } if emission_term_id else {}
1469
1468
 
1470
1469
  zero_emission_dict = {
1471
1470
  zero_emission_term_id: (
@@ -1508,7 +1507,7 @@ def create_run_function(
1508
1507
  return [
1509
1508
  new_emission_func(
1510
1509
  term_id=emission_term_id,
1511
- method_tier=total_emission.method,
1510
+ method_tier=_get_emission_method(total_emission),
1512
1511
  start_date=_get_emission_start_date(total_emission, cycle_start_date),
1513
1512
  end_date=_get_emission_end_date(total_emission, cycle_end_date),
1514
1513
  **calc_descriptive_stats(
@@ -1517,18 +1516,33 @@ def create_run_function(
1517
1516
  decimals=6
1518
1517
  )
1519
1518
  ) for emission_term_id, total_emission in assigned_emissions.items()
1519
+ if isinstance(total_emission, CarbonStockChangeEmission)
1520
1520
  ]
1521
1521
 
1522
1522
  return run
1523
1523
 
1524
1524
 
1525
+ def get_zero_emission(year):
1526
+ return CarbonStockChangeEmission(
1527
+ value=array(0),
1528
+ start_date=_gapfill_datestr(year),
1529
+ end_date=_gapfill_datestr(year, DatestrGapfillMode.END),
1530
+ method=None
1531
+ )
1532
+
1533
+
1534
+ def _get_emission_method(emission: CarbonStockChangeEmission):
1535
+ method = emission.method
1536
+ return method if isinstance(method, EmissionMethodTier) else EmissionMethodTier.TIER_1
1537
+
1538
+
1525
1539
  def _get_emission_start_date(emission: CarbonStockChangeEmission, cycle_start_date: str) -> str:
1526
1540
  cycle_datetime = safe_parse_date(_gapfill_datestr(cycle_start_date))
1527
1541
  emission_datetime = safe_parse_date(emission.start_date)
1528
1542
 
1529
1543
  should_run = (
1530
1544
  cycle_datetime and emission_datetime
1531
- and cycle_datetime <= emission_datetime # If the cycle starts before the emission, add a `startDate`
1545
+ and cycle_datetime < emission_datetime # If the cycle starts before the emission, add a `startDate`
1532
1546
  )
1533
1547
 
1534
1548
  return (
@@ -1538,12 +1552,12 @@ def _get_emission_start_date(emission: CarbonStockChangeEmission, cycle_start_da
1538
1552
 
1539
1553
 
1540
1554
  def _get_emission_end_date(emission: CarbonStockChangeEmission, cycle_end_date: str) -> str:
1541
- cycle_datetime = safe_parse_date(_gapfill_datestr(cycle_end_date))
1555
+ cycle_datetime = safe_parse_date(_gapfill_datestr(cycle_end_date, DatestrGapfillMode.END))
1542
1556
  emission_datetime = safe_parse_date(emission.end_date)
1543
1557
 
1544
1558
  should_run = (
1545
1559
  cycle_datetime and emission_datetime
1546
- and cycle_datetime >= emission_datetime # If the cycle ends after the emission, add an `endDate`
1560
+ and cycle_datetime > emission_datetime # If the cycle ends after the emission, add an `endDate`
1547
1561
  )
1548
1562
 
1549
1563
  return (
@@ -1,5 +1,5 @@
1
1
  from hestia_earth.schema import EmissionMethodTier, TermTermType
2
- from hestia_earth.utils.tools import list_sum
2
+ from hestia_earth.utils.tools import list_sum, non_empty_list
3
3
  from hestia_earth.utils.model import filter_list_term_type
4
4
 
5
5
  from hestia_earth.models.log import logRequirements, logShouldRun
@@ -16,7 +16,7 @@ REQUIREMENTS = {
16
16
  "@type": "Input",
17
17
  "value": "",
18
18
  "term.termType": "soilAmendment",
19
- "term.units": ["kg CaCO3", "kg MgCO3"]
19
+ "term.units": ["kg CaCO3", "kg CaMg(CO3)2"]
20
20
  }]
21
21
  }
22
22
  }
@@ -38,8 +38,10 @@ def _emission(value: float):
38
38
 
39
39
 
40
40
  def _get_lime_values(cycle: dict, inputs: list):
41
- # TODO: use lookup table
42
- values = [convert_to_unit(i, Units.KG_CO2) for i in inputs if len(i.get('value', [])) > 0]
41
+ values = non_empty_list([
42
+ convert_to_unit(i, Units.KG_CO2)
43
+ for i in inputs if len(i.get('value', [])) > 0
44
+ ])
43
45
  return [0] if (
44
46
  len(values) == 0 and _is_term_type_complete(cycle, TermTermType.SOILAMENDMENT)
45
47
  ) else values
@@ -56,7 +58,7 @@ def _run(CaCO3_values: list, MgCO3_values: list):
56
58
  def _should_run(cycle: dict):
57
59
  inputs = filter_list_term_type(cycle.get('inputs', []), TermTermType.SOILAMENDMENT)
58
60
  CaCO3_values = _get_lime_values(cycle, _filter_list_term_unit(inputs, Units.KG_CACO3))
59
- MgCO3_values = _get_lime_values(cycle, _filter_list_term_unit(inputs, Units.KG_MGCO3))
61
+ MgCO3_values = _get_lime_values(cycle, _filter_list_term_unit(inputs, Units.KG_CAMGCO32))
60
62
 
61
63
  logRequirements(cycle, model=MODEL, term=TERM_ID,
62
64
  CaCO3_values=len(CaCO3_values),
@@ -0,0 +1,215 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ from typing import Callable, Union
4
+
5
+ from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition
6
+
7
+ from hestia_earth.models.log import logRequirements, logShouldRun
8
+ from hestia_earth.models.utils.array_builders import gen_seed, repeat_single, truncated_normal_1d
9
+ from hestia_earth.models.utils.cycle import land_occupation_per_ha
10
+ from hestia_earth.models.utils.descriptive_stats import calc_descriptive_stats
11
+ from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
12
+ from hestia_earth.models.utils.emission import _new_emission
13
+ from hestia_earth.models.utils.measurement import most_relevant_measurement_value
14
+ from hestia_earth.models.utils.site import valid_site_type
15
+
16
+ from .organicSoilCultivation_utils import (
17
+ assign_organic_soil_category, calc_emission, get_emission_factor, OrganicSoilCategory, valid_eco_climate_zone
18
+ )
19
+ from . import MODEL
20
+
21
+ REQUIREMENTS = {
22
+ "Cycle": {
23
+ "or": [
24
+ {
25
+ "cycleDuration": "",
26
+ "practices": [{"@type": "Practice", "value": "", "term.@id": "longFallowRatio"}]
27
+ },
28
+ {
29
+ "@doc": "for plantations, additional properties are required",
30
+ "practices": [
31
+ {"@type": "Practice", "value": "", "term.@id": "nurseryDensity"},
32
+ {"@type": "Practice", "value": "", "term.@id": "nurseryDuration"},
33
+ {"@type": "Practice", "value": "", "term.@id": "plantationProductiveLifespan"},
34
+ {"@type": "Practice", "value": "", "term.@id": "plantationDensity"},
35
+ {"@type": "Practice", "value": "", "term.@id": "plantationLifespan"},
36
+ {"@type": "Practice", "value": "", "term.@id": "rotationDuration"}
37
+ ]
38
+ }
39
+ ],
40
+ "site": {
41
+ "@type": "Site",
42
+ "measurements": [
43
+ {"@type": "Measurement", "value": "", "term.@id": "histosol"},
44
+ {"@type": "Measurement", "value": "", "term.@id": "ecoClimateZone"}
45
+ ]
46
+ },
47
+ "optional": {
48
+ "cycleDuration": ""
49
+ }
50
+ }
51
+ }
52
+ LOOKUPS = {
53
+ "crop": [
54
+ "isPlantation",
55
+ "IPCC_2013_ORGANIC_SOIL_CULTIVATION_CATEGORY"
56
+ ],
57
+ "forage": [
58
+ "isPlantation",
59
+ "IPCC_2013_ORGANIC_SOIL_CULTIVATION_CATEGORY"
60
+ ],
61
+ "ecoClimateZone": [
62
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_ANNUAL_CROPS",
63
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_PERENNIAL_CROPS",
64
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_ACACIA",
65
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_OIL_PALM",
66
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_SAGO_PALM",
67
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_PADDY_RICE_CULTIVATION",
68
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_GRASSLAND",
69
+ "IPCC_2013_ORGANIC_SOILS_TONNES_CO2-C_HECTARE_OTHER"
70
+ ]
71
+ }
72
+ RETURNS = {
73
+ "Emission": [{
74
+ "value": "",
75
+ "sd": "",
76
+ "min": "",
77
+ "max": "",
78
+ "observations": "",
79
+ "statsDefinition": "simulated",
80
+ "methodTier": "tier 1"
81
+ }]
82
+ }
83
+ TERM_ID = 'co2ToAirOrganicSoilCultivation'
84
+ TIER = EmissionMethodTier.TIER_1.value
85
+
86
+ _STATS_DEFINITION = EmissionStatsDefinition.SIMULATED.value
87
+ _ITERATIONS = 100000
88
+
89
+
90
+ def _emission(descriptive_stats: dict):
91
+ emission = _new_emission(TERM_ID, MODEL) | descriptive_stats
92
+ emission['methodTier'] = TIER
93
+ return emission
94
+
95
+
96
+ def sample_emission_factor(
97
+ eco_climate_zone: EcoClimateZone,
98
+ organic_soil_category: OrganicSoilCategory,
99
+ seed: Union[int, np.random.Generator, None] = None,
100
+ ) -> npt.NDArray:
101
+ factor_data = get_emission_factor(TERM_ID, eco_climate_zone, organic_soil_category)
102
+ sample_func = _get_sample_func(factor_data)
103
+ return sample_func(iterations=_ITERATIONS, seed=seed, **factor_data)
104
+
105
+
106
+ def _sample_truncated_normal(
107
+ *, iterations: int, value: float, sd: float, seed: Union[int, np.random.Generator, None] = None, **_
108
+ ) -> npt.NDArray:
109
+ """
110
+ Randomly sample a model parameter with a truncated normal distribution. Emission factors annot be below 0, so
111
+ truncated normal sampling used.
112
+ """
113
+ return truncated_normal_1d(shape=(1, iterations), mu=value, sigma=sd, low=0, high=np.inf, seed=seed)
114
+
115
+
116
+ def _sample_constant(*, value: float, **_) -> npt.NDArray:
117
+ """Sample a constant model parameter."""
118
+ return repeat_single(shape=(1, 1), value=value)
119
+
120
+
121
+ _KWARGS_TO_SAMPLE_FUNC = {
122
+ ("value", "sd"): _sample_truncated_normal,
123
+ ("value",): _sample_constant
124
+ }
125
+ """
126
+ Mapping from available distribution data to sample function.
127
+ """
128
+
129
+
130
+ def _get_sample_func(kwargs: dict) -> Callable:
131
+ """
132
+ Select the correct sample function for a parameter based on the distribution data available. All possible
133
+ parameters for the model should have, at a minimum, a `value`, meaning that no default function needs to be
134
+ specified.
135
+
136
+ This function has been extracted into it's own method to allow for mocking of sample function.
137
+
138
+ Keyword Args
139
+ ------------
140
+ value : float
141
+ The distribution mean.
142
+ sd : float
143
+ The standard deviation of the distribution.
144
+ se : float
145
+ The standard error of the distribution.
146
+ n : float
147
+ Sample size.
148
+
149
+ Returns
150
+ -------
151
+ Callable
152
+ The sample function for the distribution.
153
+ """
154
+ return next(
155
+ sample_func for required_kwargs, sample_func in _KWARGS_TO_SAMPLE_FUNC.items()
156
+ if all(kwarg in kwargs.keys() for kwarg in required_kwargs)
157
+ )
158
+
159
+
160
+ def _should_run(cycle: dict):
161
+ end_date = cycle.get('endDate')
162
+ site = cycle.get('site', {})
163
+ measurements = site.get('measurements', [])
164
+
165
+ seed = gen_seed(cycle, MODEL, TERM_ID)
166
+ rng = np.random.default_rng(seed)
167
+
168
+ def _get_measurement_content(term_id: str):
169
+ return most_relevant_measurement_value(measurements, term_id, end_date)
170
+
171
+ histosol = _get_measurement_content('histosol')
172
+ eco_climate_zone = get_eco_climate_zone_value(cycle, as_enum=True)
173
+ organic_soil_category = assign_organic_soil_category(cycle, log_id=TERM_ID)
174
+
175
+ emission_factor = (
176
+ sample_emission_factor(eco_climate_zone, organic_soil_category, seed=rng) if eco_climate_zone
177
+ else None
178
+ )
179
+ land_occupation = land_occupation_per_ha(MODEL, TERM_ID, cycle)
180
+
181
+ logRequirements(
182
+ cycle, model=MODEL, term=TERM_ID,
183
+ eco_climate_zone=eco_climate_zone,
184
+ organic_soil_category=organic_soil_category,
185
+ emission_factor=f"{np.mean(emission_factor):.3f}",
186
+ land_occupation=land_occupation,
187
+ histosol=histosol
188
+ )
189
+
190
+ should_run = all([
191
+ valid_site_type(site),
192
+ valid_eco_climate_zone(eco_climate_zone),
193
+ all(
194
+ var is not None for var in [
195
+ emission_factor,
196
+ land_occupation,
197
+ histosol
198
+ ]
199
+ )
200
+ ])
201
+
202
+ logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
203
+
204
+ return should_run, emission_factor, histosol, land_occupation
205
+
206
+
207
+ def _run(emission_factor: npt.NDArray, histosol: float, land_occupation: float):
208
+ result = calc_emission(TERM_ID, emission_factor, histosol, land_occupation)
209
+ descriptive_stats = calc_descriptive_stats(result, _STATS_DEFINITION)
210
+ return [_emission(descriptive_stats)]
211
+
212
+
213
+ def run(cycle: dict):
214
+ should_run, emission_factor, histosol, land_occupation = _should_run(cycle)
215
+ return _run(emission_factor, histosol, land_occupation) if should_run else []
@@ -199,13 +199,11 @@ def _should_compile_inventory_func(
199
199
  ) for cycle in cycles
200
200
  )
201
201
 
202
- has_stock_measurements = len(carbon_stock_measurements) > 0
203
202
  has_cycles = len(cycles) > 0
204
203
  has_functional_unit_1_ha = all(cycle.get('functionalUnit') == CycleFunctionalUnit._1_HA.value for cycle in cycles)
205
204
 
206
205
  should_run = all([
207
206
  has_soil,
208
- has_stock_measurements,
209
207
  has_cycles,
210
208
  has_functional_unit_1_ha
211
209
  ])
@@ -214,7 +212,6 @@ def _should_compile_inventory_func(
214
212
  "site_type": site_type,
215
213
  "has_soil": has_soil,
216
214
  "carbon_stock_term": _CARBON_STOCK_TERM_ID,
217
- "has_stock_measurements": has_stock_measurements,
218
215
  "has_cycles": has_cycles,
219
216
  "has_functional_unit_1_ha": has_functional_unit_1_ha,
220
217
  }
@@ -0,0 +1,161 @@
1
+ from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition
2
+
3
+ from hestia_earth.models.log import logRequirements, logShouldRun
4
+ from hestia_earth.models.utils.cycle import land_occupation_per_ha
5
+ from hestia_earth.models.utils.ecoClimateZone import EcoClimateZone, get_eco_climate_zone_value
6
+ from hestia_earth.models.utils.emission import _new_emission
7
+ from hestia_earth.models.utils.measurement import most_relevant_measurement_value
8
+ from hestia_earth.models.utils.site import valid_site_type
9
+
10
+ from .organicSoilCultivation_utils import (
11
+ assign_organic_soil_category, calc_emission, get_emission_factor, OrganicSoilCategory, remap_categories,
12
+ valid_eco_climate_zone
13
+ )
14
+ from . import MODEL
15
+
16
+
17
+ REQUIREMENTS = {
18
+ "Cycle": {
19
+ "or": [
20
+ {
21
+ "cycleDuration": "",
22
+ "practices": [{"@type": "Practice", "value": "", "term.@id": "longFallowRatio"}]
23
+ },
24
+ {
25
+ "@doc": "for plantations, additional properties are required",
26
+ "practices": [
27
+ {"@type": "Practice", "value": "", "term.@id": "nurseryDensity"},
28
+ {"@type": "Practice", "value": "", "term.@id": "nurseryDuration"},
29
+ {"@type": "Practice", "value": "", "term.@id": "plantationProductiveLifespan"},
30
+ {"@type": "Practice", "value": "", "term.@id": "plantationDensity"},
31
+ {"@type": "Practice", "value": "", "term.@id": "plantationLifespan"},
32
+ {"@type": "Practice", "value": "", "term.@id": "rotationDuration"}
33
+ ]
34
+ }
35
+ ],
36
+ "site": {
37
+ "@type": "Site",
38
+ "measurements": [
39
+ {"@type": "Measurement", "value": "", "term.@id": "histosol"},
40
+ {"@type": "Measurement", "value": "", "term.@id": "ecoClimateZone"}
41
+ ]
42
+ },
43
+ "optional": {
44
+ "cycleDuration": ""
45
+ }
46
+ }
47
+ }
48
+ LOOKUPS = {
49
+ "crop": [
50
+ "isPlantation",
51
+ "IPCC_2013_ORGANIC_SOIL_CULTIVATION_CATEGORY"
52
+ ],
53
+ "forage": [
54
+ "isPlantation",
55
+ "IPCC_2013_ORGANIC_SOIL_CULTIVATION_CATEGORY"
56
+ ],
57
+ "ecoClimateZone": [
58
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_ANNUAL_CROPS",
59
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_PERENNIAL_CROPS",
60
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_ACACIA",
61
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_OIL_PALM",
62
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_SAGO_PALM",
63
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_PADDY_RICE_CULTIVATION",
64
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_GRASSLAND",
65
+ "IPCC_2013_ORGANIC_SOILS_TONNES_N2O-N_HECTARE_OTHER"
66
+ ]
67
+ }
68
+ RETURNS = {
69
+ "Emission": [{
70
+ "value": "",
71
+ "sd": "",
72
+ "statsDefinition": "modelled",
73
+ "methodTier": "tier 1"
74
+ }]
75
+ }
76
+ TERM_ID = 'n2OToAirOrganicSoilCultivationDirect'
77
+ TIER = EmissionMethodTier.TIER_1.value
78
+
79
+ _STATS_DEFINITION = EmissionStatsDefinition.MODELLED.value
80
+
81
+ _CATEGORY_REMAPPER = {
82
+ OrganicSoilCategory.ACACIA: OrganicSoilCategory.PERENNIAL_CROPS
83
+ }
84
+
85
+
86
+ def _emission(value: float, sd: float):
87
+ emission = _new_emission(TERM_ID, MODEL)
88
+ emission['value'] = [value]
89
+ emission['sd'] = [sd]
90
+ emission['statsDefinition'] = _STATS_DEFINITION
91
+ emission['methodTier'] = TIER
92
+ return emission
93
+
94
+
95
+ def sample_emission_factor(
96
+ eco_climate_zone: EcoClimateZone,
97
+ organic_soil_category: OrganicSoilCategory,
98
+ ) -> tuple[float, float]:
99
+ category = remap_categories(organic_soil_category, _CATEGORY_REMAPPER) # fewer categories than CO2 model
100
+ factor_data = get_emission_factor(TERM_ID, eco_climate_zone, category)
101
+ mean = factor_data.get("value", 0)
102
+ sd = factor_data.get("sd", 0)
103
+ return mean, sd
104
+
105
+
106
+ def _should_run(cycle: dict):
107
+ end_date = cycle.get('endDate')
108
+ site = cycle.get('site', {})
109
+ measurements = site.get('measurements', [])
110
+
111
+ def _get_measurement_content(term_id: str):
112
+ return most_relevant_measurement_value(measurements, term_id, end_date)
113
+
114
+ histosol = _get_measurement_content('histosol')
115
+ eco_climate_zone = get_eco_climate_zone_value(cycle, as_enum=True)
116
+ organic_soil_category = assign_organic_soil_category(cycle, log_id=TERM_ID)
117
+
118
+ emission_factor_mean, emission_factor_sd = (
119
+ sample_emission_factor(eco_climate_zone, organic_soil_category)
120
+ if eco_climate_zone
121
+ else (None, None)
122
+ )
123
+
124
+ land_occupation = land_occupation_per_ha(MODEL, TERM_ID, cycle)
125
+
126
+ logRequirements(
127
+ cycle, model=MODEL, term=TERM_ID,
128
+ eco_climate_zone=eco_climate_zone,
129
+ organic_soil_category=organic_soil_category,
130
+ emission_factor=f"{emission_factor_mean} ± {emission_factor_sd}",
131
+ land_occupation=land_occupation,
132
+ histosol=histosol
133
+ )
134
+
135
+ should_run = all([
136
+ valid_site_type(site),
137
+ valid_eco_climate_zone(eco_climate_zone),
138
+ all(
139
+ var is not None for var in [
140
+ emission_factor_mean,
141
+ emission_factor_sd,
142
+ land_occupation,
143
+ histosol
144
+ ]
145
+ )
146
+ ])
147
+
148
+ logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
149
+
150
+ return should_run, emission_factor_mean, emission_factor_sd, histosol, land_occupation
151
+
152
+
153
+ def _run(emission_factor_mean: float, emission_factor_sd: float, histosol: float, land_occupation: float):
154
+ value = calc_emission(TERM_ID, emission_factor_mean, histosol, land_occupation)
155
+ sd = calc_emission(TERM_ID, emission_factor_sd, histosol, land_occupation)
156
+ return [_emission(value, sd)]
157
+
158
+
159
+ def run(cycle: dict):
160
+ should_run, emission_factor_mean, emission_factor_sd, histosol, land_occupation = _should_run(cycle)
161
+ return _run(emission_factor_mean, emission_factor_sd, histosol, land_occupation) if should_run else []