hestia-earth-models 0.69.1__py3-none-any.whl → 0.70.1__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 (219) hide show
  1. hestia_earth/models/aware/scarcityWeightedWaterUse.py +8 -16
  2. hestia_earth/models/cache_sites.py +3 -2
  3. hestia_earth/models/cml2001Baseline/abioticResourceDepletionFossilFuels.py +2 -1
  4. hestia_earth/models/cml2001Baseline/abioticResourceDepletionMineralsAndMetals.py +3 -2
  5. hestia_earth/models/config/Cycle.json +82 -60
  6. hestia_earth/models/config/ImpactAssessment.json +12 -4
  7. hestia_earth/models/config/Site.json +33 -22
  8. hestia_earth/models/cycle/animal/input/hestiaAggregatedData.py +1 -1
  9. hestia_earth/models/cycle/animal/input/properties.py +1 -1
  10. hestia_earth/models/cycle/cycleDuration.py +2 -2
  11. hestia_earth/models/cycle/input/hestiaAggregatedData.py +12 -14
  12. hestia_earth/models/cycle/input/properties.py +1 -1
  13. hestia_earth/models/cycle/siteDuration.py +3 -3
  14. hestia_earth/models/cycle/transformation.py +1 -1
  15. hestia_earth/models/cycle/utils.py +0 -6
  16. hestia_earth/models/data/ecoinventV3/__init__.py +15 -13
  17. hestia_earth/models/ecoalimV9/__init__.py +13 -0
  18. hestia_earth/models/ecoalimV9/cycle.py +128 -0
  19. hestia_earth/models/ecoalimV9/impact_assessment.py +125 -0
  20. hestia_earth/models/ecoalimV9/utils.py +31 -0
  21. hestia_earth/models/ecoinventV3/__init__.py +6 -14
  22. hestia_earth/models/ecoinventV3/utils.py +1 -29
  23. hestia_earth/models/ecoinventV3AndEmberClimate/__init__.py +8 -2
  24. hestia_earth/models/emissionNotRelevant/__init__.py +33 -8
  25. hestia_earth/models/frischknechtEtAl2000/ionisingRadiationKbqU235Eq.py +1 -1
  26. hestia_earth/models/geospatialDatabase/croppingIntensity.py +4 -4
  27. hestia_earth/models/geospatialDatabase/longFallowRatio.py +4 -4
  28. hestia_earth/models/geospatialDatabase/region.py +3 -2
  29. hestia_earth/models/geospatialDatabase/utils.py +6 -5
  30. hestia_earth/models/haversineFormula/transport/distance.py +5 -4
  31. hestia_earth/models/{koble2014 → hestia}/aboveGroundCropResidue.py +4 -5
  32. hestia_earth/models/{cycle → hestia}/aboveGroundCropResidueTotal.py +2 -2
  33. hestia_earth/models/{site → hestia}/brackishWater.py +1 -1
  34. hestia_earth/models/{site → hestia}/cationExchangeCapacityPerKgSoil.py +1 -1
  35. hestia_earth/models/{cycle → hestia}/coldCarcassWeightPerHead.py +1 -1
  36. hestia_earth/models/{cycle → hestia}/coldDressedCarcassWeightPerHead.py +1 -1
  37. hestia_earth/models/{cycle → hestia}/concentrateFeed.py +1 -1
  38. hestia_earth/models/{cycle → hestia}/cropResidueManagement.py +1 -1
  39. hestia_earth/models/{cycle → hestia}/croppingIntensity.py +1 -1
  40. hestia_earth/models/{cycle → hestia}/energyContentLowerHeatingValue.py +2 -2
  41. hestia_earth/models/{cycle → hestia}/excretaKgMass.py +7 -2
  42. hestia_earth/models/{cycle → hestia}/excretaKgN.py +1 -1
  43. hestia_earth/models/{cycle → hestia}/excretaKgVs.py +1 -1
  44. hestia_earth/models/{cycle → hestia}/feedConversionRatio/__init__.py +1 -1
  45. hestia_earth/models/{site → hestia}/flowingWater.py +1 -1
  46. hestia_earth/models/{site → hestia}/freshWater.py +1 -1
  47. hestia_earth/models/{cycle → hestia}/inorganicFertiliser.py +1 -1
  48. hestia_earth/models/{cycle → hestia}/irrigatedTypeUnspecified.py +14 -19
  49. hestia_earth/models/hestia/landCover.py +31 -27
  50. hestia_earth/models/hestia/landTransformation100YearAverageDuringCycle.py +2 -1
  51. hestia_earth/models/hestia/landTransformation20YearAverageDuringCycle.py +2 -1
  52. hestia_earth/models/{cycle → hestia}/liveAnimal.py +1 -1
  53. hestia_earth/models/{cycle → hestia}/longFallowRatio.py +1 -1
  54. hestia_earth/models/{site → hestia}/management.py +5 -3
  55. hestia_earth/models/{cycle → hestia}/materialAndSubstrate.py +1 -1
  56. hestia_earth/models/{cycle → hestia}/milkYield.py +1 -1
  57. hestia_earth/models/{site → hestia}/netPrimaryProduction.py +1 -1
  58. hestia_earth/models/{site → hestia}/organicCarbonPerHa.py +1 -1
  59. hestia_earth/models/{cycle → hestia}/pastureGrass.py +1 -1
  60. hestia_earth/models/{cycle → hestia}/pastureSystem.py +1 -1
  61. hestia_earth/models/{site → hestia}/potentialEvapotranspirationAnnual.py +3 -3
  62. hestia_earth/models/{site → hestia}/potentialEvapotranspirationMonthly.py +3 -3
  63. hestia_earth/models/{site → hestia}/precipitationAnnual.py +3 -3
  64. hestia_earth/models/{site → hestia}/precipitationMonthly.py +3 -3
  65. hestia_earth/models/{site → hestia}/rainfallAnnual.py +3 -3
  66. hestia_earth/models/{site → hestia}/rainfallMonthly.py +3 -3
  67. hestia_earth/models/{cycle → hestia}/readyToCookWeightPerHead.py +1 -1
  68. hestia_earth/models/{cycle → hestia}/residueBurnt.py +1 -1
  69. hestia_earth/models/{cycle → hestia}/residueIncorporated.py +1 -1
  70. hestia_earth/models/{cycle → hestia}/residueLeftOnField.py +1 -1
  71. hestia_earth/models/hestia/residueRemoved.py +65 -13
  72. hestia_earth/models/{site → hestia}/salineWater.py +1 -1
  73. hestia_earth/models/hestia/seed_emissions.py +1 -1
  74. hestia_earth/models/{site → hestia}/soilMeasurement.py +1 -1
  75. hestia_earth/models/{cycle → hestia}/stockingDensityAnimalHousingAverage.py +1 -1
  76. hestia_earth/models/{site → hestia}/temperatureAnnual.py +3 -3
  77. hestia_earth/models/{site → hestia}/temperatureMonthly.py +3 -3
  78. hestia_earth/models/{site → hestia}/totalNitrogenPerKgSoil.py +1 -1
  79. hestia_earth/models/{cycle → hestia}/unknownPreSeasonWaterRegime.py +1 -1
  80. hestia_earth/models/hestia/utils.py +93 -0
  81. hestia_earth/models/{site → hestia}/waterDepth.py +1 -1
  82. hestia_earth/models/hestia/waterSalinity.py +78 -0
  83. hestia_earth/models/impact_assessment/emissions.py +1 -1
  84. hestia_earth/models/impact_assessment/product/economicValueShare.py +1 -1
  85. hestia_earth/models/impact_assessment/product/value.py +1 -1
  86. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +1 -1
  87. hestia_earth/models/ipcc2019/animal/fatContent.py +2 -2
  88. hestia_earth/models/ipcc2019/animal/milkYieldPerAnimal.py +2 -2
  89. hestia_earth/models/ipcc2019/animal/trueProteinContent.py +2 -2
  90. hestia_earth/models/ipcc2019/belowGroundBiomass.py +1 -1
  91. hestia_earth/models/ipcc2019/biomass_utils.py +2 -4
  92. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +7 -2
  93. hestia_earth/models/ipcc2019/ch4ToAirFloodedRice.py +163 -78
  94. hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChange.py +1 -0
  95. hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChange.py +2 -1
  96. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +31 -20
  97. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChange.py +2 -1
  98. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +16 -9
  99. hestia_earth/models/ipcc2019/nonCo2EmissionsToAirNaturalVegetationBurning.py +36 -47
  100. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1.py +94 -9
  101. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2.py +167 -13
  102. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +7 -5
  103. hestia_earth/models/koble2014/cropResidueManagement.py +1 -1
  104. hestia_earth/models/koble2014/residueBurnt.py +1 -1
  105. hestia_earth/models/koble2014/residueRemoved.py +1 -1
  106. hestia_earth/models/koble2014/utils.py +3 -3
  107. hestia_earth/models/mocking/search-results.json +1263 -1229
  108. hestia_earth/models/pooreNemecek2018/excretaKgN.py +1 -1
  109. hestia_earth/models/pooreNemecek2018/freshwaterWithdrawalsDuringCycle.py +1 -1
  110. hestia_earth/models/pooreNemecek2018/utils.py +12 -3
  111. hestia_earth/models/schmidt2007/ch4ToAirWasteTreatment.py +1 -6
  112. hestia_earth/models/schmidt2007/h2SToAirWasteTreatment.py +1 -6
  113. hestia_earth/models/schmidt2007/n2OToAirWasteTreatmentDirect.py +1 -6
  114. hestia_earth/models/schmidt2007/nh3ToAirWasteTreatment.py +1 -6
  115. hestia_earth/models/site/pre_checks/country.py +4 -2
  116. hestia_earth/models/transformation/input/excreta.py +1 -1
  117. hestia_earth/models/utils/aggregated.py +12 -15
  118. hestia_earth/models/utils/background_emissions.py +52 -0
  119. hestia_earth/models/utils/blank_node.py +24 -6
  120. hestia_earth/models/utils/impact_assessment.py +26 -17
  121. hestia_earth/models/utils/lookup.py +48 -39
  122. hestia_earth/models/utils/measurement.py +3 -3
  123. hestia_earth/models/utils/product.py +1 -1
  124. hestia_earth/models/utils/source.py +2 -1
  125. hestia_earth/models/utils/term.py +26 -1
  126. hestia_earth/models/version.py +1 -1
  127. {hestia_earth_models-0.69.1.dist-info → hestia_earth_models-0.70.1.dist-info}/METADATA +2 -2
  128. {hestia_earth_models-0.69.1.dist-info → hestia_earth_models-0.70.1.dist-info}/RECORD +214 -209
  129. tests/models/aware/test_scarcityWeightedWaterUse.py +1 -12
  130. tests/models/cycle/input/test_hestiaAggregatedData.py +18 -16
  131. tests/models/ecoalimV9/__init__.py +0 -0
  132. tests/models/ecoalimV9/test_cycle.py +21 -0
  133. tests/models/ecoalimV9/test_impact_assessment.py +24 -0
  134. tests/models/environmentalFootprintV3_1/test_scarcityWeightedWaterUse.py +4 -2
  135. tests/models/geospatialDatabase/test_region.py +1 -1
  136. tests/models/geospatialDatabase/test_utils.py +1 -1
  137. tests/models/haversineFormula/transport/test_distance.py +2 -2
  138. tests/models/{koble2014 → hestia}/test_aboveGroundCropResidue.py +3 -3
  139. tests/models/{cycle → hestia}/test_aboveGroundCropResidueTotal.py +1 -1
  140. tests/models/{site → hestia}/test_brackishWater.py +1 -1
  141. tests/models/{site → hestia}/test_cationExchangeCapacityPerKgSoil.py +1 -1
  142. tests/models/{cycle → hestia}/test_coldCarcassWeightPerHead.py +1 -1
  143. tests/models/{cycle → hestia}/test_coldDressedCarcassWeightPerHead.py +1 -1
  144. tests/models/{cycle → hestia}/test_concentrateFeed.py +1 -1
  145. tests/models/{cycle → hestia}/test_cropResidueManagement.py +1 -1
  146. tests/models/{cycle → hestia}/test_croppingIntensity.py +1 -1
  147. tests/models/{cycle → hestia}/test_energyContentLowerHeatingValue.py +5 -3
  148. tests/models/{cycle → hestia}/test_excretaKgMass.py +1 -1
  149. tests/models/{cycle → hestia}/test_excretaKgN.py +1 -1
  150. tests/models/{cycle → hestia}/test_excretaKgVs.py +1 -1
  151. tests/models/{cycle → hestia}/test_feedConversionRatio.py +1 -1
  152. tests/models/{site → hestia}/test_flowingWater.py +1 -1
  153. tests/models/{site → hestia}/test_freshWater.py +1 -1
  154. tests/models/{cycle → hestia}/test_inorganicFertiliser.py +1 -1
  155. tests/models/{cycle → hestia}/test_irrigatedTypeUnspecified.py +2 -5
  156. tests/models/hestia/test_landCover.py +4 -34
  157. tests/models/{cycle → hestia}/test_liveAnimal.py +1 -1
  158. tests/models/{cycle → hestia}/test_longFallowRatio.py +1 -1
  159. tests/models/{site → hestia}/test_management.py +1 -1
  160. tests/models/{cycle → hestia}/test_materialsAndSubstrate.py +1 -1
  161. tests/models/{cycle → hestia}/test_milkYield.py +1 -1
  162. tests/models/{site → hestia}/test_netPrimaryProduction.py +1 -1
  163. tests/models/{site → hestia}/test_organicCarbonPerHa.py +1 -1
  164. tests/models/{site → hestia}/test_organicCarbonPerKgSoil.py +1 -1
  165. tests/models/{site → hestia}/test_organicCarbonPerM3Soil.py +1 -1
  166. tests/models/{site → hestia}/test_organicMatterPerKgSoil.py +1 -1
  167. tests/models/{site → hestia}/test_organicMatterPerM3Soil.py +1 -1
  168. tests/models/{cycle → hestia}/test_pastureGrass.py +1 -1
  169. tests/models/{cycle → hestia}/test_pastureSystem.py +1 -1
  170. tests/models/{site → hestia}/test_potentialEvapotranspirationAnnual.py +1 -1
  171. tests/models/{site → hestia}/test_potentialEvapotranspirationMonthly.py +1 -1
  172. tests/models/{site → hestia}/test_precipitationAnnual.py +1 -1
  173. tests/models/{site → hestia}/test_precipitationMonthly.py +1 -1
  174. tests/models/{site → hestia}/test_rainfallAnnual.py +1 -1
  175. tests/models/{site → hestia}/test_rainfallMonthly.py +1 -1
  176. tests/models/{cycle → hestia}/test_readyToCookWeightPerHead.py +1 -1
  177. tests/models/{cycle → hestia}/test_residueBurnt.py +1 -1
  178. tests/models/{cycle → hestia}/test_residueIncorporated.py +1 -1
  179. tests/models/{cycle → hestia}/test_residueLeftOnField.py +1 -1
  180. tests/models/hestia/test_residueRemoved.py +15 -3
  181. tests/models/{site → hestia}/test_salineWater.py +1 -1
  182. tests/models/{site → hestia}/test_soilMeasurement.py +2 -2
  183. tests/models/{cycle → hestia}/test_stockingDensityAnimalHousingAverage.py +1 -1
  184. tests/models/{site → hestia}/test_temperatureAnnual.py +1 -1
  185. tests/models/{site → hestia}/test_temperatureMonthly.py +1 -1
  186. tests/models/{site → hestia}/test_totalNitrogenPerKgSoil.py +1 -1
  187. tests/models/{cycle → hestia}/test_unknownPreSeasonWaterRegime.py +1 -1
  188. tests/models/{site → hestia}/test_waterDepth.py +1 -1
  189. tests/models/hestia/test_waterSalinity.py +26 -0
  190. tests/models/ipcc2019/test_ch4ToAirEntericFermentation.py +11 -0
  191. tests/models/ipcc2019/test_ch4ToAirFloodedRice.py +10 -42
  192. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +2 -1
  193. tests/models/ipcc2019/test_nonCo2EmissionsToAirNaturalVegetationBurning.py +22 -8
  194. tests/models/ipcc2019/test_organicCarbonPerHa.py +4 -2
  195. tests/models/pooreNemecek2018/test_landOccupationDuringCycle.py +3 -0
  196. tests/models/site/pre_checks/test_country.py +4 -3
  197. tests/models/test_ecoinventV3.py +2 -2
  198. tests/models/test_ecoinventV3AndEmberClimate.py +2 -2
  199. tests/models/test_emissionNotRelevant.py +0 -8
  200. tests/models/utils/test_measurement.py +1 -1
  201. tests/models/utils/test_source.py +15 -5
  202. tests/orchestrator/test_models.py +1 -0
  203. hestia_earth/models/cycle/residueRemoved.py +0 -54
  204. hestia_earth/models/hestia/nh3ToSurfaceWaterAquacultureSystems.py +0 -64
  205. hestia_earth/models/site/utils.py +0 -93
  206. tests/models/cycle/test_residueRemoved.py +0 -37
  207. tests/models/hestia/test_nh3ToSurfaceWaterAquacultureSystems.py +0 -51
  208. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioCarbon.py +0 -0
  209. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioDryMatter.py +0 -0
  210. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioEnergy.py +0 -0
  211. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioFedWeight.py +0 -0
  212. /hestia_earth/models/{cycle → hestia}/feedConversionRatio/feedConversionRatioNitrogen.py +0 -0
  213. /hestia_earth/models/{site → hestia}/organicCarbonPerKgSoil.py +0 -0
  214. /hestia_earth/models/{site → hestia}/organicCarbonPerM3Soil.py +0 -0
  215. /hestia_earth/models/{site → hestia}/organicMatterPerKgSoil.py +0 -0
  216. /hestia_earth/models/{site → hestia}/organicMatterPerM3Soil.py +0 -0
  217. {hestia_earth_models-0.69.1.dist-info → hestia_earth_models-0.70.1.dist-info}/LICENSE +0 -0
  218. {hestia_earth_models-0.69.1.dist-info → hestia_earth_models-0.70.1.dist-info}/WHEEL +0 -0
  219. {hestia_earth_models-0.69.1.dist-info → hestia_earth_models-0.70.1.dist-info}/top_level.txt +0 -0
@@ -25,7 +25,7 @@ SITE_TYPE_TO_DEPTH = {
25
25
 
26
26
 
27
27
  def _measurement(site: dict, value: float):
28
- data = _new_measurement(TERM_ID)
28
+ data = _new_measurement(TERM_ID, MODEL)
29
29
  data['value'] = [value]
30
30
  data['methodClassification'] = MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS.value
31
31
  return data | get_source(site, BIBLIO_TITLE)
@@ -0,0 +1,78 @@
1
+ from hestia_earth.schema import MeasurementMethodClassification, TermTermType
2
+ from hestia_earth.utils.model import filter_list_term_type
3
+ from hestia_earth.utils.tools import safe_parse_float
4
+
5
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
6
+ from hestia_earth.models.utils.measurement import _new_measurement
7
+ from hestia_earth.models.utils.site import related_cycles
8
+ from hestia_earth.models.utils.blank_node import get_lookup_value
9
+ from . import MODEL
10
+
11
+ REQUIREMENTS = {
12
+ "Site": {
13
+ "related": {
14
+ "Cycle": {
15
+ "@type": "Cycle",
16
+ "products": [{
17
+ "@type": "Product",
18
+ "primary": "True",
19
+ "term.termType": "liveAquaticSpecies"
20
+ }]
21
+ }
22
+ }
23
+ }
24
+ }
25
+ RETURNS = {
26
+ "Measurement": [{
27
+ "value": "",
28
+ "startDate": "",
29
+ "endDate": "",
30
+ "methodClassification": "expert opinion"
31
+ }]
32
+ }
33
+ LOOKUPS = {
34
+ "liveAquaticSpecies": "defaultSalinity"
35
+ }
36
+ TERM_ID = 'waterSalinity'
37
+
38
+
39
+ def _measurement(value: float, start_date: str = None, end_date: str = None):
40
+ data = _new_measurement(TERM_ID, MODEL)
41
+ data['value'] = [value]
42
+ data['endDate'] = end_date
43
+ if start_date:
44
+ data['startDate'] = start_date
45
+ data['methodClassification'] = MeasurementMethodClassification.EXPERT_OPINION.value
46
+ return data
47
+
48
+
49
+ def _should_run(site: dict):
50
+ cycles = related_cycles(site)
51
+ relevant_products = [
52
+ {
53
+ 'product-id': product.get('term', {}).get('@id'),
54
+ 'lookup-value': safe_parse_float(
55
+ get_lookup_value(product.get('term', {}), LOOKUPS['liveAquaticSpecies']), default=None
56
+ ),
57
+ 'start-date': product.get('startDate') or cycle.get('startDate'),
58
+ 'end-date': product.get('endDate') or cycle.get('endDate')
59
+ }
60
+ for cycle in cycles
61
+ for product in filter_list_term_type(cycle.get('products', []), TermTermType.LIVEAQUATICSPECIES)
62
+ ]
63
+ has_valid_products = any([product.get('lookup-value') for product in relevant_products])
64
+
65
+ logRequirements(site, model=MODEL, term=TERM_ID,
66
+ live_aquatic_products=log_as_table(relevant_products))
67
+
68
+ should_run = all([has_valid_products])
69
+ logShouldRun(site, MODEL, TERM_ID, should_run)
70
+ return should_run, relevant_products
71
+
72
+
73
+ def run(site: dict):
74
+ should_run, values = _should_run(site)
75
+ return [
76
+ _measurement(value.get('lookup-value'), value.get('start-date'), value.get('end-date'))
77
+ for value in values if value.get('lookup-value') is not None
78
+ ] if should_run else []
@@ -8,7 +8,7 @@ from . import MODEL
8
8
 
9
9
  REQUIREMENTS = {
10
10
  "ImpactAssessment": {
11
- "product": {"@type": "Product", "term": {"@type": "Term"}},
11
+ "product": {"@type": "Product"},
12
12
  "cycle": {
13
13
  "@type": "Cycle",
14
14
  "products": [{
@@ -4,7 +4,7 @@ from .. import MODEL
4
4
 
5
5
  REQUIREMENTS = {
6
6
  "ImpactAssessment": {
7
- "product": {"@type": "Product", "term": {"@type": "Term"}, "none": {"economicValueShare": ""}},
7
+ "product": {"@type": "Product", "none": {"economicValueShare": ""}},
8
8
  "cycle": {
9
9
  "@type": "Cycle",
10
10
  "products": [{"@type": "Product", "economicValueShare": ""}]
@@ -4,7 +4,7 @@ from .. import MODEL
4
4
 
5
5
  REQUIREMENTS = {
6
6
  "ImpactAssessment": {
7
- "product": {"@type": "Product", "term": {"@type": "Term"}, "none": {"value": ""}},
7
+ "product": {"@type": "Product", "none": {"value": ""}},
8
8
  "cycle": {
9
9
  "@type": "Cycle",
10
10
  "products": [{"@type": "Product", "value": ""}]
@@ -568,7 +568,7 @@ def _measurement(
568
568
  "dates": [f"{year}-12-31" for year in timestamps],
569
569
  "methodClassification": _METHOD_CLASSIFICATION
570
570
  }
571
- measurement = _new_measurement(TERM_ID) | {
571
+ measurement = _new_measurement(TERM_ID, MODEL) | {
572
572
  key: value for key, value in update_dict.items() if value
573
573
  }
574
574
  return measurement
@@ -9,10 +9,10 @@ REQUIREMENTS = {
9
9
  "animals": [{
10
10
  "@type": "Animal",
11
11
  "term.termType": "liveAnimal",
12
- "practices": {
12
+ "practices": [{
13
13
  "@type": "Practice",
14
14
  "term.termType": "animalManagement"
15
- }
15
+ }]
16
16
  }]
17
17
  }
18
18
  }
@@ -13,10 +13,10 @@ REQUIREMENTS = {
13
13
  "animals": [{
14
14
  "@type": "Animal",
15
15
  "term.termType": "liveAnimal",
16
- "practices": {
16
+ "practices": [{
17
17
  "@type": "Practice",
18
18
  "term.termType": "animalManagement"
19
- }
19
+ }]
20
20
  }]
21
21
  }
22
22
  }
@@ -9,10 +9,10 @@ REQUIREMENTS = {
9
9
  "animals": [{
10
10
  "@type": "Animal",
11
11
  "term.termType": "liveAnimal",
12
- "practices": {
12
+ "practices": [{
13
13
  "@type": "Practice",
14
14
  "term.termType": "animalManagement"
15
- }
15
+ }]
16
16
  }]
17
17
  }
18
18
  }
@@ -551,7 +551,7 @@ def _measurement(
551
551
  "dates": [f"{year}-12-31" for year in timestamps],
552
552
  "methodClassification": _METHOD_CLASSIFICATION
553
553
  }
554
- measurement = _new_measurement(TERM_ID) | {
554
+ measurement = _new_measurement(TERM_ID, MODEL) | {
555
555
  key: value for key, value in update_dict.items() if value
556
556
  }
557
557
  measurement["depthUpper"] = _DEPTH_UPPER
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
  from functools import reduce
3
3
  from math import isclose
4
- from numpy import random
4
+ from numpy import inf, random
5
5
  from numpy.typing import NDArray
6
6
  from typing import Callable, Optional, Union
7
7
 
@@ -393,9 +393,7 @@ def sample_plus_minus_error(
393
393
  ) -> NDArray:
394
394
  """Randomly sample a model parameter with a truncated normal distribution described using plus/minus error."""
395
395
  sd = value * (error / 200)
396
- low = value - (value * (error / 100))
397
- high = value + (value * (error / 100))
398
- return truncated_normal_1d(shape=(1, iterations), mu=value, sigma=sd, low=low, high=high, seed=seed)
396
+ return truncated_normal_1d(shape=(1, iterations), mu=value, sigma=sd, low=0, high=inf, seed=seed)
399
397
 
400
398
 
401
399
  def sample_constant(*, iterations: int, value: float, **_) -> NDArray:
@@ -5,6 +5,7 @@ from hestia_earth.utils.tools import list_sum, safe_parse_float
5
5
 
6
6
  from hestia_earth.models.log import debugMissingLookup, debugValues, logRequirements, logShouldRun, log_as_table
7
7
  from hestia_earth.models.utils.blank_node import get_total_value_converted_with_min_ratio
8
+ from hestia_earth.models.utils.term import get_ionophore_terms
8
9
  from hestia_earth.models.utils.input import get_feed_inputs
9
10
  from hestia_earth.models.utils.emission import _new_emission
10
11
  from hestia_earth.models.utils.liveAnimal import get_default_digestibility
@@ -48,7 +49,10 @@ REQUIREMENTS = {
48
49
  }
49
50
  }]
50
51
  }
51
- ]
52
+ ],
53
+ "optional": {
54
+ "inputs": [{"@type": "Input", "term.@id": ["ionophores", "ionophoreAntibiotics"]}]
55
+ }
52
56
  }
53
57
  }
54
58
  LOOKUPS = {
@@ -192,7 +196,8 @@ def _get_DE_type(lookup, term_id: str, term_type: str):
192
196
 
193
197
  def _is_ionophore(cycle: dict, total_feed: float):
194
198
  inputs = cycle.get('inputs', [])
195
- has_input = find_term_match(inputs, 'ionophores', None) is not None
199
+ ionophore_terms = get_ionophore_terms()
200
+ has_input = any([find_term_match(inputs, term_id, None) is not None for term_id in ionophore_terms])
196
201
  maize_input = find_term_match(inputs, 'maizeSteamFlaked')
197
202
  maize_feed = get_total_value_converted_with_min_ratio(MODEL, None, blank_nodes=[maize_input]) if maize_input else 0
198
203
  maize_feed_ratio = maize_feed / total_feed if all([maize_feed, total_feed]) else 0
@@ -1,8 +1,10 @@
1
+ from functools import reduce
1
2
  from hestia_earth.schema import EmissionMethodTier, EmissionStatsDefinition, TermTermType
2
3
  from hestia_earth.utils.model import filter_list_term_type, find_term_match
3
- from hestia_earth.utils.tools import list_sum, safe_parse_float
4
+ from hestia_earth.utils.tools import list_sum, safe_parse_float, non_empty_list
4
5
 
5
- from hestia_earth.models.log import debugValues, logRequirements, logShouldRun
6
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table, debugValues
7
+ from hestia_earth.models.utils import multiply_values
6
8
  from hestia_earth.models.utils.term import get_lookup_value
7
9
  from hestia_earth.models.utils.emission import _new_emission
8
10
  from hestia_earth.models.utils.product import has_flooded_rice
@@ -12,18 +14,17 @@ from . import MODEL
12
14
 
13
15
  REQUIREMENTS = {
14
16
  "Cycle": {
15
- "practices": [{"@type": "Practice", "value": "", "term.@id": "croppingDuration"}],
17
+ "practices": [
18
+ {"@type": "Practice", "value": "", "term.@id": "croppingDuration"},
19
+ {"@type": "Practice", "value": "", "term.termType": ["landUseManagement", "waterRegime"]}
20
+ ],
16
21
  "site": {
17
22
  "@type": "Site",
18
23
  "country": {"@type": "Term", "termType": "region"}
19
24
  },
20
25
  "optional": {
21
26
  "inputs": [
22
- {
23
- "@type": "Input",
24
- "value": "",
25
- "term.termType": "organicFertiliser"
26
- },
27
+ {"@type": "Input", "value": "", "term.termType": "organicFertiliser"},
27
28
  {
28
29
  "@type": "Input",
29
30
  "value": "",
@@ -32,28 +33,35 @@ REQUIREMENTS = {
32
33
  }
33
34
  ],
34
35
  "products": [{"@type": "Product", "value": "", "term.@id": "aboveGroundCropResidueIncorporated"}],
35
- "practices": [
36
- {"@type": "Practice", "value": "", "term.termType": "cropResidueManagement"},
37
- {"@type": "Practice", "value": "", "term.termType": "landUseManagement"},
38
- {"@type": "Practice", "value": "", "term.termType": "waterRegime"}
39
- ]
36
+ "practices": [{"@type": "Practice", "value": "", "term.termType": "cropResidueManagement"}]
40
37
  }
41
38
  }
42
39
  }
43
40
  LOOKUPS = {
44
41
  "landUseManagement": [
45
- "IPCC_2019_CH4_rice_SFw", "IPCC_2019_CH4_rice_SFw-min", "IPCC_2019_CH4_rice_SFw-max",
46
- "IPCC_2019_CH4_rice_SFw-sd",
47
- "IPCC_2019_CH4_rice_SFp", "IPCC_2019_CH4_rice_SFp-min", "IPCC_2019_CH4_rice_SFp-max",
42
+ "IPCC_2019_CH4_rice_SFp",
43
+ "IPCC_2019_CH4_rice_SFp-min",
44
+ "IPCC_2019_CH4_rice_SFp-max",
48
45
  "IPCC_2019_CH4_rice_SFp-sd"
49
46
  ],
50
47
  "waterRegime": [
51
- "IPCC_2019_CH4_rice_SFw", "IPCC_2019_CH4_rice_SFw-min", "IPCC_2019_CH4_rice_SFw-max",
52
- "IPCC_2019_CH4_rice_SFw-sd",
53
- "IPCC_2019_CH4_rice_SFp", "IPCC_2019_CH4_rice_SFp-min", "IPCC_2019_CH4_rice_SFp-max",
54
- "IPCC_2019_CH4_rice_SFp-sd"
48
+ "IPCC_2019_CH4_rice_SFw",
49
+ "IPCC_2019_CH4_rice_SFw-min",
50
+ "IPCC_2019_CH4_rice_SFw-max",
51
+ "IPCC_2019_CH4_rice_SFw-sd"
52
+ ],
53
+ "organicFertiliser": [
54
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight",
55
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight_min",
56
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight_max",
57
+ "IPCC_2019_CH4_rice_CFOA_kg_fresh_weight_sd"
58
+ ],
59
+ "cropResidueManagement": [
60
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight",
61
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight_min",
62
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight_max",
63
+ "IPCC_2019_CH4_rice_CFOA_kg_dry_weight_sd"
55
64
  ],
56
- "organicFertiliser": ["IPCC_2019_CH4_rice_CFOA_kg_fresh_weight", "IPCC_2019_CH4_rice_CFOA_kg_dry_weight"],
57
65
  "region-ch4ef-IPCC2019": ["CH4_ef", "CH4_ef_min", "CH4_ef_max", "CH4_ef_sd"]
58
66
  }
59
67
  RETURNS = {
@@ -68,92 +76,138 @@ RETURNS = {
68
76
  }
69
77
  TERM_ID = 'ch4ToAirFloodedRice'
70
78
  TIER = EmissionMethodTier.TIER_1.value
79
+ _STATS = ['value', 'min', 'max', 'sd']
71
80
 
72
81
 
73
82
  def _emission(value: float, min: float, max: float, sd: float):
74
83
  emission = _new_emission(TERM_ID, MODEL)
75
84
  emission['value'] = [value]
76
- emission['min'] = [min]
77
- emission['max'] = [max]
78
- emission['sd'] = [sd]
85
+ if min is not None:
86
+ emission['min'] = [min]
87
+ if max is not None:
88
+ emission['max'] = [max]
89
+ if sd is not None:
90
+ emission['sd'] = [sd]
79
91
  emission['methodTier'] = TIER
80
92
  emission['statsDefinition'] = EmissionStatsDefinition.MODELLED.value
81
93
  return emission
82
94
 
83
95
 
84
- def _get_CH4_ef(country: str, suffix: str = ''):
96
+ def _get_CH4_ef(country: str, suffix: str = 'value'):
85
97
  lookup_name = 'region-ch4ef-IPCC2019.csv'
98
+ lookup = 'CH4_ef'
99
+ lookup = '_'.join([lookup, suffix]) if suffix != 'value' else lookup
86
100
  return safe_parse_float(
87
- get_region_lookup_value(lookup_name, country, 'CH4_ef' + suffix, model=MODEL, term=TERM_ID)
101
+ get_region_lookup_value(lookup_name, country, lookup, model=MODEL, term=TERM_ID),
102
+ default=None
88
103
  )
89
104
 
90
105
 
91
- def _get_practice_lookup(term: dict, col: str):
92
- return safe_parse_float(get_lookup_value(term, col, model=MODEL, term=TERM_ID))
93
-
94
-
95
- def _get_cropResidue_value(cycle: dict, suffix: str = ''):
106
+ def _get_cropResidue_value(cycle: dict, suffix: str = 'value'):
107
+ product_id = 'aboveGroundCropResidueIncorporated'
96
108
  abgIncorporated = list_sum(
97
- find_term_match(cycle.get('products', []), 'aboveGroundCropResidueIncorporated').get('value', [])
109
+ find_term_match(cycle.get('products', []), product_id).get('value', []),
110
+ default=None
98
111
  )
99
112
  abgManagement = filter_list_term_type(cycle.get('practices', []), TermTermType.CROPRESIDUEMANAGEMENT)
100
113
  term = abgManagement[0].get('term', {}) if len(abgManagement) > 0 else None
114
+ lookup = 'IPCC_2019_CH4_rice_CFOA_kg_dry_weight'
115
+ lookup = '_'.join([lookup, suffix]) if suffix != 'value' else lookup
101
116
  factor = safe_parse_float(
102
- get_lookup_value(term, LOOKUPS['organicFertiliser'][1] + suffix, model=MODEL, term=TERM_ID)
103
- ) if term else 0
104
- return abgIncorporated * factor
117
+ get_lookup_value(term, lookup, model=MODEL, term=TERM_ID),
118
+ default=None
119
+ ) if term else None
105
120
 
121
+ debugValues(cycle, model=MODEL, term=TERM_ID,
122
+ **{'cropResidue_' + suffix: log_as_table({
123
+ 'product-id': product_id,
124
+ 'product-value': abgIncorporated,
125
+ 'factor': factor
126
+ })})
106
127
 
107
- def _get_fertiliser_value(input: dict, suffix: str = ''):
128
+ return multiply_values([abgIncorporated, factor])
129
+
130
+
131
+ def _get_fertiliser_values(input: dict, suffix: str = 'value'):
108
132
  term = input.get('term', {})
133
+ lookup = 'IPCC_2019_CH4_rice_CFOA_kg_fresh_weight'
134
+ lookup = '_'.join([lookup, suffix]) if suffix != 'value' else lookup
109
135
  factor = safe_parse_float(
110
- get_lookup_value(term, LOOKUPS['organicFertiliser'][0] + suffix, model=MODEL, term=TERM_ID)
136
+ get_lookup_value(term, lookup, model=MODEL, term=TERM_ID),
137
+ default=None
111
138
  )
112
- return list_sum(input.get('value', [])) * factor
139
+ value = list_sum(input.get('value', []))
140
+ return {'input-id': term.get('@id'), 'input-value': value, 'factor': factor}
113
141
 
114
142
 
115
- def _calculate_SFo(cycle: dict, suffix: str = ''):
116
- cropResidue = _get_cropResidue_value(cycle, suffix)
117
- fertilisers = get_organicFertiliser_inputs(cycle)
118
- fert_value = list_sum([_get_fertiliser_value(i, suffix) for i in fertilisers])
119
- return (1 + (fert_value/1000) + (cropResidue/1000)) ** 0.59
143
+ def _get_fertiliser_value(cycle: dict, suffix: str = 'value'):
144
+ fertiliser_values = [_get_fertiliser_values(i, suffix) for i in get_organicFertiliser_inputs(cycle)]
120
145
 
146
+ debugValues(cycle, model=MODEL, term=TERM_ID,
147
+ **{'fertiliser_' + suffix: log_as_table(fertiliser_values)})
121
148
 
122
- def _calculate_SF_average(practices: list, factor: str):
123
- values = [
124
- (_get_practice_lookup(p.get('term', {}), factor), list_sum(p.get('value', []), None)) for p in practices
149
+ valid_fertiliser_values = [
150
+ value for value in fertiliser_values
151
+ if all([value.get('input-value') is not None, value.get('factor') is not None])
125
152
  ]
126
- # sum only values that are numbers
127
- return list_sum([factor * percent / 100 for factor, percent in values if percent is not None])
153
+ fert_value = list_sum([
154
+ value.get('input-value') * value.get('factor')
155
+ for value in valid_fertiliser_values
156
+ ])
157
+ return fert_value
128
158
 
129
159
 
130
- def _calculate_factor(cycle: dict, country: str, practices: list, suffix: str = ''):
131
- CH4_ef = _get_CH4_ef(country, suffix)
132
- SFw = _calculate_SF_average(practices, 'IPCC_2019_CH4_rice_SFw' + suffix)
133
- SFp = _calculate_SF_average(practices, 'IPCC_2019_CH4_rice_SFp' + suffix)
134
- SFo = _calculate_SFo(cycle, suffix)
135
- debugValues(cycle, model=MODEL, term=TERM_ID, **{
136
- 'CH4_ef' + suffix: CH4_ef,
137
- 'SFw' + suffix: SFw,
138
- 'SFp' + suffix: SFp,
139
- 'SFo' + suffix: SFo
140
- })
141
- return CH4_ef * (SFw if SFw > 0 else 1) * (SFp if SFp > 0 else 1) * SFo
160
+ def _calculate_SFo(cycle: dict, suffix: str = 'value'):
161
+ cropResidue = _get_cropResidue_value(cycle, suffix)
162
+ fertiliser = _get_fertiliser_value(cycle, suffix)
142
163
 
164
+ return (1 + (fertiliser/1000) + (cropResidue/1000)) ** 0.59
143
165
 
144
- def _get_croppingDuration(croppingDuration: dict, key: str = 'value'):
145
- return list_sum(croppingDuration.get(key, croppingDuration.get('value', [])))
146
166
 
167
+ def _get_practice_values(practice: dict, col: str, default=None):
168
+ term = practice.get('term', {})
169
+ factor = safe_parse_float(get_lookup_value(term, col, model=MODEL, term=TERM_ID), default)
170
+ return {
171
+ 'practice-id': term.get('@id'),
172
+ 'factor': factor,
173
+ 'practice-value': list_sum(practice.get('value', []), default=default)
174
+ } if factor is not None else None
147
175
 
148
- def _run(cycle: dict, croppingDuration: dict, country: str):
149
- practices = filter_list_term_type(cycle.get('practices', []), [
150
- TermTermType.WATERREGIME, TermTermType.LANDUSEMANAGEMENT
151
- ])
152
176
 
153
- value = _calculate_factor(cycle, country, practices) * _get_croppingDuration(croppingDuration)
154
- min = _calculate_factor(cycle, country, practices, '_min') * _get_croppingDuration(croppingDuration, 'min')
155
- max = _calculate_factor(cycle, country, practices, '_max') * _get_croppingDuration(croppingDuration, 'max')
156
- sd = (max-min)/4
177
+ def _calculate_SF_total(cycle: dict, practices: list, lookup: str, suffix: str = 'value', default=None):
178
+ lookup_column = '-'.join([lookup, suffix]) if suffix != 'value' else lookup
179
+ values = non_empty_list([_get_practice_values(p, lookup_column) for p in practices])
180
+
181
+ debugValues(cycle, model=MODEL, term=TERM_ID,
182
+ **{lookup_column: log_as_table(values)})
183
+
184
+ used_values = [value for value in values if value.get('practice-value') is not None]
185
+
186
+ # sum only values that are numbers
187
+ return (
188
+ list_sum([
189
+ value.get('factor') * value.get('practice-value') for value in used_values
190
+ ], default=None) / list_sum([
191
+ value.get('practice-value') for value in used_values
192
+ ])
193
+ ) if used_values else (
194
+ default if suffix == 'value' else None
195
+ )
196
+
197
+
198
+ def _value_from_factors(values: list, key: str = 'value'):
199
+ # get the value from all factors, and only run if all are provided
200
+ all_values = [value.get(key) for value in values]
201
+ return multiply_values(all_values) if all([v is not None for v in all_values]) else None
202
+
203
+
204
+ def _run(values: list):
205
+ value = _value_from_factors(values, 'value')
206
+ min = _value_from_factors(values, 'min')
207
+ max = _value_from_factors(values, 'max')
208
+ sd = _value_from_factors(values, 'sd')
209
+
210
+ sd = (max-min)/4 if all([max, min]) else None
157
211
 
158
212
  return [_emission(value, min, max, sd)]
159
213
 
@@ -162,20 +216,51 @@ def _should_run(cycle: dict):
162
216
  country = cycle.get('site', {}).get('country', {}).get('@id')
163
217
 
164
218
  flooded_rice = has_flooded_rice(cycle.get('products', []))
219
+ practices = cycle.get('practices', [])
165
220
 
166
- croppingDuration = find_term_match(cycle.get('practices', []), 'croppingDuration', None)
221
+ croppingDuration = find_term_match(practices, 'croppingDuration', None)
167
222
  has_croppingDuration = croppingDuration is not None
223
+ croppingDuration = reduce(lambda p, key: p | {
224
+ key: list_sum(croppingDuration.get(key) or [], default=None)
225
+ }, _STATS, {}) if has_croppingDuration else {}
226
+
227
+ CH4_ef = reduce(lambda p, key: p | {key: _get_CH4_ef(country, key)}, _STATS, {})
228
+ SFo = reduce(lambda p, key: p | {key: _calculate_SFo(cycle, key)}, _STATS, {})
229
+
230
+ water_regime = filter_list_term_type(practices, TermTermType.WATERREGIME)
231
+ SFw = reduce(lambda p, key: p | {
232
+ key: _calculate_SF_total(cycle, water_regime, 'IPCC_2019_CH4_rice_SFw', key)
233
+ }, _STATS, {})
234
+
235
+ land_use_management = filter_list_term_type(practices, TermTermType.LANDUSEMANAGEMENT)
236
+ SFp = reduce(lambda p, key: p | {
237
+ key: _calculate_SF_total(cycle, land_use_management, 'IPCC_2019_CH4_rice_SFp', key, default=1)
238
+ }, _STATS, {})
168
239
 
169
240
  logRequirements(cycle, model=MODEL, term=TERM_ID,
170
241
  has_flooded_rice=flooded_rice,
171
- has_croppingDuration=has_croppingDuration,
172
- country=country)
173
-
174
- should_run = all([flooded_rice, has_croppingDuration, country])
242
+ country=country,
243
+ values=log_as_table([
244
+ {'name': 'croppingDuration'} | croppingDuration,
245
+ {'name': 'CH4-ef'} | CH4_ef,
246
+ {'name': 'SFo'} | SFo,
247
+ {'name': 'SFw'} | SFw,
248
+ {'name': 'SFp'} | SFp,
249
+ ]))
250
+
251
+ should_run = all([
252
+ flooded_rice,
253
+ has_croppingDuration,
254
+ country,
255
+ CH4_ef.get('value') is not None,
256
+ SFo.get('value') is not None,
257
+ SFw.get('value') is not None,
258
+ SFp.get('value') is not None,
259
+ ])
175
260
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
176
- return should_run, croppingDuration, country
261
+ return should_run, [croppingDuration, CH4_ef, SFo, SFw, SFp]
177
262
 
178
263
 
179
264
  def run(cycle: dict):
180
- should_run, croppingDuration, country = _should_run(cycle)
181
- return _run(cycle, croppingDuration, country) if should_run else []
265
+ should_run, values = _should_run(cycle)
266
+ return _run(values) if should_run else []
@@ -11,6 +11,7 @@ from . import MODEL
11
11
  REQUIREMENTS = {
12
12
  "Cycle": {
13
13
  "site": {
14
+ "@type": "Site",
14
15
  "measurements": [
15
16
  {
16
17
  "@type": "Measurement",
@@ -11,6 +11,7 @@ from . import MODEL
11
11
  REQUIREMENTS = {
12
12
  "Cycle": {
13
13
  "site": {
14
+ "@type": "Site",
14
15
  "measurements": [
15
16
  {
16
17
  "@type": "Measurement",
@@ -38,7 +39,7 @@ RETURNS = {
38
39
  "statsDefinition": "simulated",
39
40
  "observations": "",
40
41
  "methodTier": "",
41
- "depth": "30"
42
+ "depth": 30
42
43
  }]
43
44
  }
44
45
  TERM_ID = 'co2ToAirBelowGroundBiomassStockChangeLandUseChange,co2ToAirBelowGroundBiomassStockChangeManagementChange'
@@ -796,26 +796,8 @@ def _preprocess_carbon_stocks(
796
796
  list[CarbonStock]
797
797
  A list of carbon stocks sorted by date.
798
798
  """
799
- sorted_measurements = sorted(
800
- flatten([split_node_by_dates(m) for m in carbon_stock_measurements]),
801
- key=lambda node: _gapfill_datestr(node["dates"][0], DatestrGapfillMode.END)
802
- )
803
-
804
- values = flatten(node["value"] for node in sorted_measurements)
805
-
806
- sds = flatten(
807
- node.get("sd", []) or [_calc_nominal_sd(v, _NOMINAL_ERROR) for v in node["value"]]
808
- for node in sorted_measurements
809
- )
810
-
811
- dates = flatten(
812
- [_gapfill_datestr(datestr, DatestrGapfillMode.END) for datestr in node["dates"]]
813
- for node in sorted_measurements
814
- )
815
-
816
- methods = flatten(
817
- [MeasurementMethodClassification(node.get("methodClassification")) for _ in node["value"]]
818
- for node in sorted_measurements
799
+ dates, values, sds, methods = _extract_node_data(
800
+ flatten([split_node_by_dates(m) for m in carbon_stock_measurements])
819
801
  )
820
802
 
821
803
  correlation_matrix = compute_time_series_correlation_matrix(
@@ -842,6 +824,35 @@ def _preprocess_carbon_stocks(
842
824
  ]
843
825
 
844
826
 
827
+ def _extract_node_data(nodes: list[dict]) -> list[dict]:
828
+
829
+ def group_node(result, node) -> dict[str, dict]:
830
+ date = _gapfill_datestr(node["dates"][0], DatestrGapfillMode.END)
831
+ result[date] = result.get(date, []) + [node]
832
+ return result
833
+
834
+ grouped_nodes = reduce(group_node, nodes, dict())
835
+
836
+ def get_values(date):
837
+ return flatten(node.get("value", []) for node in grouped_nodes[date])
838
+
839
+ def get_sds(date):
840
+ return flatten(
841
+ node.get("sd", []) or [_calc_nominal_sd(v, _NOMINAL_ERROR) for v in node.get("value", [])]
842
+ for node in grouped_nodes[date]
843
+ )
844
+
845
+ def get_methods(date):
846
+ return flatten(node.get("methodClassification", []) for node in grouped_nodes[date])
847
+
848
+ dates = sorted(grouped_nodes.keys())
849
+ values = [mean(get_values(date)) for date in dates]
850
+ sds = [mean(get_sds(date)) for date in dates]
851
+ methods = [min_measurement_method_classification(get_methods(date)) for date in dates]
852
+
853
+ return dates, values, sds, methods
854
+
855
+
845
856
  def _calc_nominal_sd(value: float, error: float) -> float:
846
857
  """
847
858
  Calculate a nominal SD for a carbon stock measurement. Can be used to gap fill SD when information not present in