hestia-earth-models 0.64.12__py3-none-any.whl → 0.64.14__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 (190) hide show
  1. hestia_earth/models/aware/scarcityWeightedWaterUse.py +1 -1
  2. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsLandOccupation.py +1 -1
  3. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsLandTransformation.py +1 -1
  4. hestia_earth/models/chaudharyBrooks2018/damageToTerrestrialEcosystemsTotalLandUseEffects.py +3 -2
  5. hestia_earth/models/cml2001Baseline/eutrophicationPotentialExcludingFate.py +2 -2
  6. hestia_earth/models/cml2001Baseline/terrestrialAcidificationPotentialIncludingFateAverageEurope.py +2 -2
  7. hestia_earth/models/cml2001NonBaseline/eutrophicationPotentialIncludingFateAverageEurope.py +2 -2
  8. hestia_earth/models/cml2001NonBaseline/terrestrialAcidificationPotentialExcludingFate.py +2 -2
  9. hestia_earth/models/cycle/completeness/seed.py +6 -4
  10. hestia_earth/models/cycle/concentrateFeed.py +9 -4
  11. hestia_earth/models/cycle/endDate.py +10 -1
  12. hestia_earth/models/cycle/materialAndSubstrate.py +158 -0
  13. hestia_earth/models/cycle/milkYield.py +6 -5
  14. hestia_earth/models/cycle/startDate.py +6 -4
  15. hestia_earth/models/edip2003/ozoneDepletionPotential.py +2 -2
  16. hestia_earth/models/emissionNotRelevant/__init__.py +3 -2
  17. hestia_earth/models/environmentalFootprintV3/freshwaterEcotoxicityPotentialCtue.py +2 -3
  18. hestia_earth/models/hestia/landCover.py +28 -26
  19. hestia_earth/models/ipcc2013ExcludingFeedbacks/gwp100.py +2 -2
  20. hestia_earth/models/ipcc2013IncludingFeedbacks/gwp100.py +2 -2
  21. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +4 -8
  22. hestia_earth/models/ipcc2019/animal/milkYieldPerAnimal.py +2 -2
  23. hestia_earth/models/ipcc2019/belowGroundBiomass.py +4 -8
  24. hestia_earth/models/ipcc2019/biomass_utils.py +11 -0
  25. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +3 -3
  26. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -2
  27. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +7 -3
  28. hestia_earth/models/ipcc2019/n2OToAirExcretaDirect.py +14 -9
  29. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +9 -3
  30. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1 -2
  31. hestia_earth/models/koble2014/aboveGroundCropResidue.py +5 -1
  32. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsClimateChange.py +2 -2
  33. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  34. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  35. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealth.py +2 -2
  36. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthClimateChange.py +2 -2
  37. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  38. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  39. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  40. hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  41. hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsPdfYear.py +2 -2
  42. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  43. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  44. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  45. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsClimateChange.py +2 -2
  46. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  47. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  48. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealth.py +2 -2
  49. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
  50. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  51. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  52. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  53. hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  54. hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsPdfYear.py +2 -2
  55. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  56. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  57. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  58. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  59. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  60. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealth.py +2 -2
  61. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthClimateChange.py +2 -2
  62. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  63. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  64. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  65. hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  66. hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsPdfYear.py +2 -2
  67. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  68. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  69. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  70. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  71. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  72. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealth.py +2 -2
  73. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
  74. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  75. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  76. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  77. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  78. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsPdfYear.py +2 -2
  79. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  80. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  81. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  82. hestia_earth/models/linkedImpactAssessment/emissions.py +3 -0
  83. hestia_earth/models/log.py +4 -3
  84. hestia_earth/models/mocking/__init__.py +1 -1
  85. hestia_earth/models/mocking/search-results.json +1021 -1021
  86. hestia_earth/models/pooreNemecek2018/excretaKgN.py +4 -4
  87. hestia_earth/models/pooreNemecek2018/excretaKgVs.py +4 -4
  88. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterCropResidueDecomposition.py +1 -1
  89. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterExcreta.py +1 -1
  90. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterInorganicFertiliser.py +1 -1
  91. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterOrganicFertiliser.py +1 -1
  92. hestia_earth/models/pooreNemecek2018/{saplings.py → saplingsDepreciatedAmountPerCycle.py} +1 -1
  93. hestia_earth/models/pooreNemecek2018/utils.py +7 -1
  94. hestia_earth/models/preload_requests.py +24 -4
  95. hestia_earth/models/recipe2016Egalitarian/damageToFreshwaterEcosystemsSpeciesYear.py +2 -2
  96. hestia_earth/models/recipe2016Egalitarian/damageToHumanHealth.py +2 -2
  97. hestia_earth/models/recipe2016Egalitarian/damageToMarineEcosystemsSpeciesYear.py +2 -2
  98. hestia_earth/models/recipe2016Egalitarian/damageToTerrestrialEcosystemsSpeciesYear.py +2 -2
  99. hestia_earth/models/recipe2016Egalitarian/freshwaterAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  100. hestia_earth/models/recipe2016Egalitarian/freshwaterEutrophicationPotential.py +2 -2
  101. hestia_earth/models/recipe2016Egalitarian/humanCarcinogenicToxicity.py +2 -3
  102. hestia_earth/models/recipe2016Egalitarian/humanNonCarcinogenicToxicity.py +2 -3
  103. hestia_earth/models/recipe2016Egalitarian/marineAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  104. hestia_earth/models/recipe2016Egalitarian/marineEutrophicationPotential.py +2 -2
  105. hestia_earth/models/recipe2016Egalitarian/ozoneDepletionPotential.py +2 -2
  106. hestia_earth/models/recipe2016Egalitarian/terrestrialAcidificationPotential.py +2 -2
  107. hestia_earth/models/recipe2016Egalitarian/terrestrialEcotoxicityPotential14Dcbeq.py +2 -3
  108. hestia_earth/models/recipe2016Hierarchist/damageToFreshwaterEcosystemsSpeciesYear.py +2 -2
  109. hestia_earth/models/recipe2016Hierarchist/damageToHumanHealth.py +2 -2
  110. hestia_earth/models/recipe2016Hierarchist/damageToMarineEcosystemsSpeciesYear.py +2 -2
  111. hestia_earth/models/recipe2016Hierarchist/damageToTerrestrialEcosystemsSpeciesYear.py +2 -2
  112. hestia_earth/models/recipe2016Hierarchist/freshwaterAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  113. hestia_earth/models/recipe2016Hierarchist/freshwaterEutrophicationPotential.py +2 -2
  114. hestia_earth/models/recipe2016Hierarchist/humanCarcinogenicToxicity.py +2 -3
  115. hestia_earth/models/recipe2016Hierarchist/humanNonCarcinogenicToxicity.py +2 -3
  116. hestia_earth/models/recipe2016Hierarchist/marineAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  117. hestia_earth/models/recipe2016Hierarchist/marineEutrophicationPotential.py +2 -2
  118. hestia_earth/models/recipe2016Hierarchist/ozoneDepletionPotential.py +2 -2
  119. hestia_earth/models/recipe2016Hierarchist/terrestrialAcidificationPotential.py +2 -2
  120. hestia_earth/models/recipe2016Hierarchist/terrestrialEcotoxicityPotential14Dcbeq.py +2 -3
  121. hestia_earth/models/recipe2016Individualist/damageToFreshwaterEcosystemsSpeciesYear.py +2 -2
  122. hestia_earth/models/recipe2016Individualist/damageToHumanHealth.py +2 -2
  123. hestia_earth/models/recipe2016Individualist/damageToMarineEcosystemsSpeciesYear.py +2 -2
  124. hestia_earth/models/recipe2016Individualist/damageToTerrestrialEcosystemsSpeciesYear.py +2 -2
  125. hestia_earth/models/recipe2016Individualist/freshwaterAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  126. hestia_earth/models/recipe2016Individualist/freshwaterEutrophicationPotential.py +2 -2
  127. hestia_earth/models/recipe2016Individualist/humanCarcinogenicToxicity.py +2 -3
  128. hestia_earth/models/recipe2016Individualist/humanNonCarcinogenicToxicity.py +2 -3
  129. hestia_earth/models/recipe2016Individualist/marineAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  130. hestia_earth/models/recipe2016Individualist/marineEutrophicationPotential.py +2 -2
  131. hestia_earth/models/recipe2016Individualist/ozoneDepletionPotential.py +2 -2
  132. hestia_earth/models/recipe2016Individualist/terrestrialAcidificationPotential.py +2 -2
  133. hestia_earth/models/recipe2016Individualist/terrestrialEcotoxicityPotential14Dcbeq.py +2 -3
  134. hestia_earth/models/site/management.py +142 -144
  135. hestia_earth/models/stehfestBouwman2006/n2OToAirCropResidueDecompositionDirect.py +1 -1
  136. hestia_earth/models/stehfestBouwman2006/n2OToAirExcretaDirect.py +1 -1
  137. hestia_earth/models/stehfestBouwman2006/n2OToAirInorganicFertiliserDirect.py +1 -1
  138. hestia_earth/models/stehfestBouwman2006/n2OToAirOrganicFertiliserDirect.py +1 -1
  139. hestia_earth/models/stehfestBouwman2006/noxToAirCropResidueDecomposition.py +1 -1
  140. hestia_earth/models/stehfestBouwman2006/noxToAirExcreta.py +1 -1
  141. hestia_earth/models/stehfestBouwman2006/noxToAirInorganicFertiliser.py +1 -1
  142. hestia_earth/models/stehfestBouwman2006/noxToAirOrganicFertiliser.py +1 -1
  143. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirCropResidueDecomposition.py +1 -1
  144. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirExcreta.py +1 -1
  145. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirInorganicFertiliser.py +1 -1
  146. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirOrganicFertiliser.py +1 -1
  147. hestia_earth/models/usetoxV2/freshwaterEcotoxicityPotentialCtue.py +2 -3
  148. hestia_earth/models/utils/__init__.py +4 -1
  149. hestia_earth/models/utils/blank_node.py +34 -14
  150. hestia_earth/models/utils/constant.py +3 -0
  151. hestia_earth/models/utils/emission.py +1 -8
  152. hestia_earth/models/utils/management.py +11 -0
  153. hestia_earth/models/utils/pesticideAI.py +11 -17
  154. hestia_earth/models/utils/term.py +2 -1
  155. hestia_earth/models/version.py +1 -1
  156. {hestia_earth_models-0.64.12.dist-info → hestia_earth_models-0.64.14.dist-info}/METADATA +4 -4
  157. {hestia_earth_models-0.64.12.dist-info → hestia_earth_models-0.64.14.dist-info}/RECORD +186 -187
  158. {hestia_earth_models-0.64.12.dist-info → hestia_earth_models-0.64.14.dist-info}/WHEEL +1 -1
  159. tests/models/cycle/completeness/test_seed.py +1 -1
  160. tests/models/cycle/test_endDate.py +18 -2
  161. tests/models/cycle/test_materialsAndSubstrate.py +49 -0
  162. tests/models/cycle/test_startDate.py +21 -3
  163. tests/models/hestia/test_landCover.py +3 -2
  164. tests/models/ipcc2019/test_aboveGroundBiomass.py +2 -1
  165. tests/models/ipcc2019/test_belowGroundBiomass.py +2 -1
  166. tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChange.py +4 -3
  167. tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChange.py +3 -3
  168. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +1 -1
  169. tests/models/ipcc2019/test_n2OToAirExcretaDirect.py +12 -0
  170. tests/models/ipcc2019/test_organicCarbonPerHa.py +1 -0
  171. tests/models/koble2014/test_aboveGroundCropResidue.py +13 -0
  172. tests/models/pooreNemecek2018/test_no3ToGroundwaterCropResidueDecomposition.py +3 -2
  173. tests/models/pooreNemecek2018/test_no3ToGroundwaterExcreta.py +3 -2
  174. tests/models/pooreNemecek2018/test_no3ToGroundwaterInorganicFertiliser.py +3 -2
  175. tests/models/pooreNemecek2018/test_no3ToGroundwaterOrganicFertiliser.py +3 -2
  176. tests/models/pooreNemecek2018/{test_saplings.py → test_saplingsDepreciatedAmountPerCycle.py} +1 -1
  177. tests/models/site/test_management.py +18 -151
  178. tests/models/utils/test_blank_node.py +57 -1
  179. tests/models/utils/test_emission.py +1 -6
  180. tests/models/utils/test_site.py +33 -2
  181. tests/models/pooreNemecek2018/test_no3ToGroundwaterSoilFlux.py +0 -90
  182. tests/models/stehfestBouwman2006/test_n2OToAirSoilFlux.py +0 -41
  183. tests/models/stehfestBouwman2006/test_noxToAirSoilFlux.py +0 -40
  184. tests/models/stehfestBouwman2006GisImplementation/test_noxToAirSoilFlux.py +0 -33
  185. /hestia_earth/models/pooreNemecek2018/{no3ToGroundwaterSoilFlux.py → no3ToGroundwaterSoilFlux_utils.py} +0 -0
  186. /hestia_earth/models/stehfestBouwman2006/{n2OToAirSoilFlux.py → n2OToAirSoilFlux_utils.py} +0 -0
  187. /hestia_earth/models/stehfestBouwman2006/{noxToAirSoilFlux.py → noxToAirSoilFlux_utils.py} +0 -0
  188. /hestia_earth/models/stehfestBouwman2006GisImplementation/{noxToAirSoilFlux.py → noxToAirSoilFlux_utils.py} +0 -0
  189. {hestia_earth_models-0.64.12.dist-info → hestia_earth_models-0.64.14.dist-info}/LICENSE +0 -0
  190. {hestia_earth_models-0.64.12.dist-info → hestia_earth_models-0.64.14.dist-info}/top_level.txt +0 -0
@@ -14,18 +14,19 @@ When nodes are chronologically consecutive with "% area" or "boolean" units and
14
14
  condensed into a single node to aid readability.
15
15
  """
16
16
  from functools import reduce
17
-
18
- from hestia_earth.schema import SchemaType, TermTermType, SiteSiteType
19
- from hestia_earth.utils.api import download_hestia
20
- from hestia_earth.utils.model import filter_list_term_type, linked_node
21
- from hestia_earth.utils.tools import safe_parse_float, flatten, non_empty_list
17
+ from hestia_earth.schema import TermTermType, SiteSiteType
18
+ from hestia_earth.utils.model import filter_list_term_type
19
+ from hestia_earth.utils.tools import safe_parse_float, flatten
22
20
  from hestia_earth.utils.blank_node import get_node_value
23
21
 
24
- from hestia_earth.models.log import logRequirements, logShouldRun, log_blank_nodes_id
22
+ from hestia_earth.models.log import logRequirements, logShouldRun, log_as_table
23
+ from hestia_earth.models.utils import _include
24
+ from hestia_earth.models.utils.management import _new_management
25
25
  from hestia_earth.models.utils.term import get_lookup_value
26
26
  from hestia_earth.models.utils.blank_node import condense_nodes
27
- from hestia_earth.models.utils.site import related_cycles
28
- from hestia_earth.models.utils.site import get_land_cover_term_id as get_landCover_term_id_from_site_type
27
+ from hestia_earth.models.utils.site import (
28
+ related_cycles, get_land_cover_term_id as get_landCover_term_id_from_site_type
29
+ )
29
30
  from . import MODEL
30
31
 
31
32
  REQUIREMENTS = {
@@ -89,36 +90,37 @@ LOOKUPS = {
89
90
  "landUseManagement": "GAP_FILL_TO_MANAGEMENT"
90
91
  }
91
92
  MODEL_KEY = 'management'
92
- LAND_COVER_KEY = LOOKUPS['crop'][0]
93
- ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
94
- INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
95
- ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserUsed"
96
- AMENDMENT_INCREASING_C_USED_TERM_ID = "amendmentIncreasingSoilCarbonUsed"
97
- INPUT_RULES = {
93
+
94
+ _LAND_COVER_KEY = LOOKUPS['crop'][0]
95
+ _ANIMAL_MANURE_USED_TERM_ID = "animalManureUsed"
96
+ _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID = "inorganicNitrogenFertiliserUsed"
97
+ _ORGANIC_FERTILISER_USED_TERM_ID = "organicFertiliserUsed"
98
+ _AMENDMENT_INCREASING_C_USED_TERM_ID = "amendmentIncreasingSoilCarbonUsed"
99
+ _INPUT_RULES = {
98
100
  TermTermType.INORGANICFERTILISER.value: (
99
101
  (
100
102
  TermTermType.INORGANICFERTILISER.value, # Lookup column
101
103
  lambda x: safe_parse_float(x) > 0, # Condition
102
- INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID # New term.
104
+ _INORGANIC_NITROGEN_FERTILISER_USED_TERM_ID # New term.
103
105
  ),
104
106
  ),
105
107
  TermTermType.SOILAMENDMENT.value: (
106
108
  (
107
109
  TermTermType.SOILAMENDMENT.value,
108
- lambda x: x is True,
109
- AMENDMENT_INCREASING_C_USED_TERM_ID
110
+ lambda x: bool(x) is True,
111
+ _AMENDMENT_INCREASING_C_USED_TERM_ID
110
112
  ),
111
113
  ),
112
114
  TermTermType.ORGANICFERTILISER.value: (
113
115
  (
114
116
  TermTermType.SOILAMENDMENT.value,
115
- lambda x: x is True,
116
- ORGANIC_FERTILISER_USED_TERM_ID
117
+ lambda x: bool(x) is True,
118
+ _ORGANIC_FERTILISER_USED_TERM_ID
117
119
  ),
118
120
  (
119
121
  TermTermType.ORGANICFERTILISER.value,
120
- lambda x: x is True,
121
- ANIMAL_MANURE_USED_TERM_ID
122
+ lambda x: bool(x) is True,
123
+ _ANIMAL_MANURE_USED_TERM_ID
122
124
  )
123
125
  )
124
126
  }
@@ -128,15 +130,28 @@ _SKIP_LAND_COVER_SITE_TYPES = [
128
130
 
129
131
 
130
132
  def management(data: dict):
131
- node = {'@type': SchemaType.MANAGEMENT.value}
132
- return node | data
133
+ node = _new_management(data.get('id'))
134
+ node['value'] = data['value']
135
+ node['endDate'] = data['endDate']
136
+ if data.get('startDate'):
137
+ node['startDate'] = data['startDate']
138
+ if data.get('properties'):
139
+ node['properties'] = data['properties']
140
+ return node
133
141
 
134
142
 
135
- def _extract_node_value(node: dict) -> dict:
136
- return node | {'value': get_node_value(node)}
143
+ def _map_to_value(value: dict):
144
+ return {
145
+ 'id': value.get('term', {}).get('@id'),
146
+ 'value': value.get('value'),
147
+ 'startDate': value.get('startDate'),
148
+ 'endDate': value.get('endDate'),
149
+ 'properties': value.get('properties')
150
+ }
137
151
 
138
152
 
139
- def _include(value: dict, keys: list) -> dict: return {k: v for k, v in value.items() if k in keys}
153
+ def _extract_node_value(node: dict) -> dict:
154
+ return node | {'value': get_node_value(node)}
140
155
 
141
156
 
142
157
  def _default_dates(cycle: dict, values: list):
@@ -155,148 +170,121 @@ def _copy_item_if_exists(source: dict, keys: list[str] = None, dest: dict = None
155
170
 
156
171
 
157
172
  def _get_landCover_term_id(product: dict) -> str:
158
- value = get_lookup_value(product.get('term', {}), LAND_COVER_KEY, model=MODEL, model_key=LAND_COVER_KEY)
173
+ term = product.get('term', {})
174
+ value = get_lookup_value(term, _LAND_COVER_KEY, model=MODEL, term=term.get('@id'), model_key=MODEL_KEY)
159
175
  return value.split(';')[0] if value else None
160
176
 
161
177
 
162
178
  def _get_relevant_items(
163
- cycles: list[dict], item_name: str, relevant_terms: list, date_fill: callable = _default_dates
179
+ cycle: dict, item_name: str, relevant_terms: list, date_fill: callable = _default_dates
164
180
  ):
165
181
  """
166
182
  Get items from the list of cycles with any of the relevant terms.
167
183
  Also adds dates if missing.
168
184
  """
169
185
  return [
170
- [
171
- item
172
- for item in date_fill(cycle=cycle, values=filter_list_term_type(cycle.get(item_name, []), relevant_terms))
173
- ]
174
- for cycle in cycles
186
+ item
187
+ for item in date_fill(cycle=cycle, values=filter_list_term_type(cycle.get(item_name, []), relevant_terms))
175
188
  ]
176
189
 
177
190
 
178
- def _get_lookup_with_debug(term: dict, column: str) -> any:
179
- get_lookup_value(term, column, model_key=MODEL_KEY, land_cover_key=LAND_COVER_KEY)
180
-
181
-
182
- def _data_from_input(cycle: dict, term_id: str) -> dict:
183
- return {
184
- "term": {
185
- "@type": "Term",
186
- "@id": term_id,
187
- "termType": "landUseManagement"
188
- },
189
- "value": True,
190
- "startDate": cycle["startDate"],
191
- "endDate": cycle["endDate"]
192
- }
193
-
194
-
195
- def _process_rule(cycle, term, term_type) -> list:
191
+ def _process_rule(node: dict, term: dict) -> list:
196
192
  relevant_terms = []
197
- for column, condition, new_term in INPUT_RULES[term_type]:
198
- lookup_result = _get_lookup_with_debug(term, LOOKUPS[column])
193
+ for column, condition, new_term in _INPUT_RULES[term.get('termType')]:
194
+ lookup_result = get_lookup_value(term, LOOKUPS[column], model=MODEL, term=term.get('@id'), model_key=MODEL_KEY)
199
195
 
200
196
  if condition(lookup_result):
201
- relevant_terms.append(_data_from_input(cycle=cycle, term_id=new_term))
197
+ relevant_terms.append(node | {'id': new_term})
202
198
 
203
199
  return relevant_terms
204
200
 
205
201
 
206
- def _get_relevant_inputs(cycles: list[dict]) -> list:
207
- relevant_inputs = []
208
- for cycle in [c for c in cycles if "inputs" in c]:
209
- for i in cycle["inputs"]:
210
- if i.get("term", {}).get("termType", "") in INPUT_RULES:
211
- relevant_inputs.extend(
212
- _process_rule(
213
- cycle=cycle,
214
- term=i.get("term", {}),
215
- term_type=i.get("term", {}).get("termType", "")
216
- )
217
- )
218
-
219
- return relevant_inputs
220
-
221
-
222
- def _has_gap_fill_to_management_set(practices: list) -> list:
223
- """
224
- Include only landUseManagement practices where GAP_FILL_TO_MANAGEMENT = True
225
- """
226
- result = [
227
- p for p in practices
228
- if p.get("term", {}).get("termType", {}) != TermTermType.LANDUSEMANAGEMENT.value
229
- or get_lookup_value(lookup_term=p.get("term", {}), column=LOOKUPS["landUseManagement"])
230
- ]
231
- return result
232
-
233
-
234
- def _should_run_all_products(cycles: list, site_type: str):
235
- products_land_cover = flatten(_get_relevant_items(
236
- cycles=cycles,
202
+ def _run_from_inputs(site: dict, cycle: dict) -> list:
203
+ inputs = flatten([
204
+ _process_rule(node={
205
+ 'value': True,
206
+ 'startDate': cycle.get('startDate'),
207
+ 'endDate': cycle.get('endDate')
208
+ }, term=input.get('term'))
209
+ for input in cycle.get('inputs', [])
210
+ if input.get('term', {}).get('termType') in _INPUT_RULES
211
+ ])
212
+ return inputs
213
+
214
+
215
+ def _run_from_siteType(site: dict, cycle: dict):
216
+ site_type = site.get('siteType')
217
+ site_type_id = get_landCover_term_id_from_site_type(site_type) if site_type not in _SKIP_LAND_COVER_SITE_TYPES \
218
+ else None
219
+
220
+ should_run = all([site_type_id])
221
+ return [{
222
+ 'id': site_type_id,
223
+ 'value': 100,
224
+ 'startDate': cycle.get('startDate'),
225
+ 'endDate': cycle.get('endDate')
226
+ }] if should_run else []
227
+
228
+
229
+ def _run_from_landCover(cycle: dict):
230
+ products = _get_relevant_items(
231
+ cycle=cycle,
237
232
  item_name="products",
238
233
  relevant_terms=[TermTermType.LANDCOVER]
239
- )) if site_type else []
240
- products_land_cover = [
241
- _extract_node_value(
234
+ )
235
+ products = [
236
+ _map_to_value(_extract_node_value(
242
237
  _include(
243
238
  value=product,
244
239
  keys=["term", "value", "startDate", "endDate", "properties"]
245
240
  )
246
- ) for product in products_land_cover
241
+ )) for product in products
247
242
  ]
243
+ return products
244
+
248
245
 
249
- products_crop_forage = _get_relevant_items(
250
- cycles=cycles,
246
+ def _run_from_crop_forage(cycle: dict):
247
+ products = _get_relevant_items(
248
+ cycle=cycle,
251
249
  item_name="products",
252
250
  relevant_terms=[TermTermType.CROP, TermTermType.FORAGE],
253
251
  date_fill=_dates_from_current_cycle
254
252
  )
255
- products_crop_forage = [
256
- _copy_item_if_exists(
253
+ products = list(filter(_get_landCover_term_id, products))
254
+ products = [
255
+ _map_to_value(_copy_item_if_exists(
257
256
  source=product,
258
257
  keys=["startDate", "endDate", "properties"],
259
258
  dest={
260
- "term": linked_node(download_hestia(_get_landCover_term_id(product))),
261
- "value": round(100 / len(_products), 2)
259
+ "term": {'@id': _get_landCover_term_id(product)},
260
+ "value": round(100 / len(products), 2)
262
261
  }
263
- )
264
- for _products in products_crop_forage
265
- for product in list(filter(_get_landCover_term_id, _products))
266
- ] if site_type else []
267
- dates = sorted(list(set(
268
- non_empty_list(flatten([[cycle.get('startDate'), cycle.get('endDate')] for cycle in cycles]))
269
- ))) if site_type not in _SKIP_LAND_COVER_SITE_TYPES else []
270
- site_type_id = get_landCover_term_id_from_site_type(site_type) if site_type else None
271
- site_type_term = download_hestia(site_type_id) if all([len(dates) >= 2, site_type_id]) else None
272
- products_site_type = [{
273
- "term": linked_node(site_type_term),
274
- "value": 100,
275
- "startDate": dates[0],
276
- "endDate": dates[-1]
277
- }] if site_type_term else []
278
-
279
- return products_site_type, products_crop_forage, products_land_cover
280
-
281
-
282
- def _should_run(site: dict):
283
- cycles = related_cycles(site)
262
+ ))
263
+ for product in products
264
+ ]
265
+ return products
266
+
284
267
 
285
- products_animal, products_crop_forage, products_land_cover = _should_run_all_products(
286
- cycles=cycles,
287
- site_type=site.get("siteType")
268
+ def _has_gap_fill_to_management_set(practice: dict):
269
+ """
270
+ Include only landUseManagement practices where GAP_FILL_TO_MANAGEMENT = True
271
+ """
272
+ term = practice.get('term', {})
273
+ return (
274
+ term.get('termType') != TermTermType.LANDUSEMANAGEMENT.value or
275
+ get_lookup_value(lookup_term=term, column=LOOKUPS["landUseManagement"])
288
276
  )
289
- all_products = products_land_cover + products_crop_forage + products_animal
290
- all_products = condense_nodes(all_products)
291
277
 
278
+
279
+ def _run_from_practices(cycle: dict):
292
280
  practices = [
293
281
  _extract_node_value(
294
282
  _include(
295
283
  value=practice,
296
284
  keys=["term", "value", "startDate", "endDate"]
297
285
  )
298
- ) for practice in flatten(_get_relevant_items(
299
- cycles=cycles,
286
+ ) for practice in _get_relevant_items(
287
+ cycle=cycle,
300
288
  item_name="practices",
301
289
  relevant_terms=[
302
290
  TermTermType.WATERREGIME,
@@ -305,28 +293,38 @@ def _should_run(site: dict):
305
293
  TermTermType.LANDUSEMANAGEMENT,
306
294
  TermTermType.SYSTEM
307
295
  ]
308
- ))
296
+ )
297
+ ]
298
+ practices = list(map(_map_to_value, filter(_has_gap_fill_to_management_set, practices)))
299
+ return practices
300
+
301
+
302
+ def _run_cycle(site: dict, cycle: dict):
303
+ inputs = _run_from_inputs(site, cycle)
304
+ products = _run_from_landCover(cycle) + _run_from_crop_forage(cycle)
305
+ site_types = _run_from_siteType(site, cycle)
306
+ practices = _run_from_practices(cycle)
307
+ return [
308
+ node | {'cycle-id': cycle.get('@id')}
309
+ for node in inputs + products + site_types + practices
309
310
  ]
310
- practices = _has_gap_fill_to_management_set(practices)
311
- practices = condense_nodes(practices)
312
-
313
- relevant_inputs = _get_relevant_inputs(cycles)
314
- logRequirements(
315
- site,
316
- model=MODEL,
317
- term=None,
318
- model_key=MODEL_KEY,
319
- products_crop_forage_ids=log_blank_nodes_id(products_crop_forage),
320
- products_land_cover_ids=log_blank_nodes_id(products_land_cover),
321
- products_animal=log_blank_nodes_id(products_animal),
322
- practice_ids=log_blank_nodes_id(practices),
323
- inputs=log_blank_nodes_id(relevant_inputs)
324
- )
325
- should_run = any(all_products + practices + relevant_inputs)
326
- logShouldRun(site, MODEL, None, should_run=should_run, model_key=MODEL_KEY)
327
- return should_run, all_products, practices, relevant_inputs
328
311
 
329
312
 
330
313
  def run(site: dict):
331
- should_run, products, practices, inputs = _should_run(site)
332
- return list(map(management, products + practices + inputs)) if should_run else []
314
+ cycles = related_cycles(site)
315
+ nodes = flatten([_run_cycle(site, cycle) for cycle in cycles])
316
+
317
+ # group nodes with same `id` to display as a single log per node
318
+ grouped_nodes = reduce(lambda p, c: p | {c['id']: p.get(c['id'], []) + [c]}, nodes, {})
319
+ for id, values in grouped_nodes.items():
320
+ logRequirements(
321
+ site,
322
+ model=MODEL,
323
+ term=id,
324
+ model_key=MODEL_KEY,
325
+ details=log_as_table(values, ignore_keys=['id', 'properties']),
326
+ )
327
+ logShouldRun(site, MODEL, id, True, model_key=MODEL_KEY)
328
+
329
+ management_nodes = condense_nodes(list(map(management, nodes)))
330
+ return management_nodes
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_crop_residue_decomposition_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .n2OToAirSoilFlux import _should_run, _get_value
7
+ from .n2OToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_excreta_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .n2OToAirSoilFlux import _get_value, _should_run
7
+ from .n2OToAirSoilFlux_utils import _get_value, _should_run
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.emission import _new_emission
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.cycle import get_inorganic_fertiliser_N_total
7
- from .n2OToAirSoilFlux import _get_value, _should_run
7
+ from .n2OToAirSoilFlux_utils import _get_value, _should_run
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.emission import _new_emission
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.cycle import get_organic_fertiliser_N_total
7
- from .n2OToAirSoilFlux import _get_value, _should_run
7
+ from .n2OToAirSoilFlux_utils import _get_value, _should_run
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_crop_residue_decomposition_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_excreta_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_inorganic_fertiliser_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_organic_fertiliser_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_crop_residue_decomposition_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_excreta_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_inorganic_fertiliser_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -4,7 +4,7 @@ from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.cycle import get_organic_fertiliser_N_total
5
5
  from hestia_earth.models.utils.completeness import _is_term_type_complete
6
6
  from hestia_earth.models.utils.emission import _new_emission
7
- from .noxToAirSoilFlux import _should_run, _get_value
7
+ from .noxToAirSoilFlux_utils import _should_run, _get_value
8
8
  from . import MODEL
9
9
 
10
10
  REQUIREMENTS = {
@@ -1,5 +1,5 @@
1
1
  from hestia_earth.models.utils.indicator import _new_indicator
2
- from hestia_earth.models.utils.pesticideAI import impact_lookup_value, impact_value_set_none
2
+ from hestia_earth.models.utils.pesticideAI import impact_lookup_value
3
3
  from . import MODEL
4
4
 
5
5
  REQUIREMENTS = {
@@ -30,5 +30,4 @@ def _indicator(value: float):
30
30
 
31
31
  def run(impact_assessment: dict):
32
32
  value = impact_lookup_value(MODEL, TERM_ID, impact_assessment, LOOKUPS['pesticideAI'])
33
- should_run = any([value is not None, impact_value_set_none(impact_assessment)])
34
- return _indicator(value) if should_run else None
33
+ return _indicator(value) if value is not None else None
@@ -29,7 +29,10 @@ def cached_value(node: dict, key: str = None, default=None):
29
29
  def _term_id(term): return term.get('@id') if isinstance(term, dict) else term
30
30
 
31
31
 
32
- def _omit(values: dict, keys: list): return {k: v for k, v in values.items() if k not in keys}
32
+ def _omit(values: dict, keys: list) -> dict: return {k: v for k, v in values.items() if k not in keys}
33
+
34
+
35
+ def _include(value: dict, keys: list) -> dict: return {k: v for k, v in value.items() if k in keys}
33
36
 
34
37
 
35
38
  def _include_model(node: dict, term_id: str):
@@ -280,9 +280,9 @@ def get_total_value_converted_with_min_ratio(
280
280
  ) for blank_node in blank_nodes
281
281
  ]
282
282
  value_logs = log_as_table([{
283
- 'id': term_id,
284
- 'value': value,
285
- prop_id: prop_value
283
+ 'node-id': term_id,
284
+ 'node-value': value,
285
+ f"{prop_id}-value": prop_value
286
286
  } for term_id, value, prop_value in values])
287
287
 
288
288
  total_value = list_sum([value for term_id, value, prop_value in values])
@@ -746,7 +746,7 @@ DATESTR_GAPFILL_MODE_TO_GAPFILL_FUNCTION = {
746
746
 
747
747
  def _gapfill_datestr(datestr: str, mode: DatestrGapfillMode = DatestrGapfillMode.START) -> str:
748
748
  """
749
- Gapfill incomplete datestrs and returns them in the format `YYYY-MM-DDTHH:MM_SS`.
749
+ Gapfill incomplete datestrs and returns them in the format `YYYY-MM-DDTHH:mm:ss`.
750
750
  """
751
751
  VALID_DATE_FORMATS = {
752
752
  DatestrFormat.YEAR, DatestrFormat.YEAR_MONTH, DatestrFormat.YEAR_MONTH_DAY
@@ -768,7 +768,7 @@ def _datetime_range_duration(range: DatetimeRange, add_second=False) -> float:
768
768
  """
769
769
  Determine the length of a `DatetimeRange` in seconds.
770
770
 
771
- Option to `add_second` to account for 1 second between 23:59:59 and 00:00:00)
771
+ Option to `add_second` to account for 1 second between 23:59:59 and 00:00:00
772
772
  """
773
773
  return (range.end - range.start).total_seconds() + int(add_second)
774
774
 
@@ -779,7 +779,7 @@ def _calc_datetime_range_intersection_duration(
779
779
  """
780
780
  Determine the length of a `DatetimeRange` in seconds.
781
781
 
782
- Option to `add_second` to account for 1 second between 23:59:59 and 00:00:00)
782
+ Option to `add_second` to account for 1 second between 23:59:59 and 00:00:00
783
783
  """
784
784
  latest_start = max(range_a.start, range_b.start)
785
785
  earliest_end = min(range_a.end, range_b.end)
@@ -793,7 +793,7 @@ def _calc_datetime_range_intersection_duration(
793
793
 
794
794
  # if less than 0 the ranges do not intersect, so return 0.
795
795
  return (
796
- _datetime_range_duration(intersection_range) + int(add_second)
796
+ _datetime_range_duration(intersection_range, add_second=add_second)
797
797
  if duration > 0 else 0
798
798
  )
799
799
 
@@ -829,7 +829,10 @@ def _should_run_node_by_end_date(node: dict) -> bool:
829
829
  """
830
830
  Validate nodes for `group_nodes_by_year` using the "startDate" and "endDate" fields.
831
831
  """
832
- return _get_datestr_format(node.get("endDate")) in VALID_DATE_FORMATS_GROUP_NODES_BY_YEAR
832
+ return (
833
+ _get_datestr_format(node.get("endDate")) in VALID_DATE_FORMATS_GROUP_NODES_BY_YEAR
834
+ and validate_start_date_end_date(node)
835
+ )
833
836
 
834
837
 
835
838
  def _should_run_node_by_dates(node: dict) -> bool:
@@ -927,8 +930,8 @@ def _build_time_fraction_dict(
927
930
  node_datetime_range, group_datetime_range, add_second=True
928
931
  )
929
932
 
930
- fraction_of_group_duration = intersection_duration / group_duration
931
- fraction_of_node_duration = intersection_duration / node_duration
933
+ fraction_of_group_duration = intersection_duration / group_duration if group_duration > 0 else 0
934
+ fraction_of_node_duration = intersection_duration / node_duration if node_duration > 0 else 0
932
935
 
933
936
  return {
934
937
  "fraction_of_group_duration": fraction_of_group_duration,
@@ -1283,10 +1286,19 @@ def _group_nodes_by_consecutive_dates(nodes: list):
1283
1286
 
1284
1287
  def _node_from_group(nodes: list):
1285
1288
  # `nodes` contain list with consecutive dates
1286
- return nodes[0] if len(nodes) == 1 else nodes[0] | {
1287
- 'startDate': min(n.get('startDate') for n in nodes),
1288
- 'endDate': max(n.get('endDate') for n in nodes)
1289
- }
1289
+ # if all nodes have the same dates, sum up the values
1290
+ same_startDate = len(set([n.get('startDate') for n in nodes])) == 1
1291
+ same_endDate = len(set([n.get('endDate') for n in nodes])) == 1
1292
+ total_value = list_sum(flatten([n.get('value', []) for n in nodes]))
1293
+ return nodes[0] if len(nodes) == 1 else (
1294
+ nodes[0] | {
1295
+ 'value': [total_value] if isinstance(nodes[0]['value'], list) else total_value
1296
+ } if all([same_startDate, same_endDate])
1297
+ else nodes[0] | {
1298
+ 'startDate': min(n.get('startDate') for n in nodes),
1299
+ 'endDate': max(n.get('endDate') for n in nodes)
1300
+ }
1301
+ )
1290
1302
 
1291
1303
 
1292
1304
  def _condense_nodes(nodes: list):
@@ -1457,3 +1469,11 @@ def convert_unit_properties(node_value: Union[int, float], node: dict, dest_unit
1457
1469
  lambda value, conversion_property_field: _convert_via_property(node, value, conversion_property_field),
1458
1470
  conversions, node_value
1459
1471
  ) if conversions else None
1472
+
1473
+
1474
+ def validate_start_date_end_date(node: dict) -> bool:
1475
+ """Return `True` if `node.startDate` is before `node.endDate`, `False` if otherwise."""
1476
+ start_date = _gapfill_datestr(node.get("startDate", OLDEST_DATE))
1477
+ end_date = _gapfill_datestr(node.get("endDate", OLDEST_DATE), DatestrGapfillMode.END)
1478
+
1479
+ return safe_parse_date(start_date) < safe_parse_date(end_date)
@@ -2,6 +2,9 @@ from enum import Enum
2
2
 
3
3
  from hestia_earth.utils.tools import list_sum
4
4
 
5
+ DAYS_IN_YEAR = 365.25
6
+ DAYS_PER_MONTH = DAYS_IN_YEAR/12
7
+
5
8
 
6
9
  class Units(Enum):
7
10
  BOOLEAN = 'boolean'