hestia-earth-models 0.64.11__py3-none-any.whl → 0.64.13__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 (196) 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 +36 -19
  11. hestia_earth/models/cycle/endDate.py +10 -1
  12. hestia_earth/models/cycle/milkYield.py +6 -5
  13. hestia_earth/models/cycle/startDate.py +6 -4
  14. hestia_earth/models/edip2003/ozoneDepletionPotential.py +2 -2
  15. hestia_earth/models/emissionNotRelevant/__init__.py +3 -2
  16. hestia_earth/models/environmentalFootprintV3/freshwaterEcotoxicityPotentialCtue.py +2 -3
  17. hestia_earth/models/faostat2018/utils.py +72 -12
  18. hestia_earth/models/hestia/__init__.py +13 -0
  19. hestia_earth/models/hestia/landCover.py +727 -0
  20. hestia_earth/models/ipcc2013ExcludingFeedbacks/gwp100.py +2 -2
  21. hestia_earth/models/ipcc2013IncludingFeedbacks/gwp100.py +2 -2
  22. hestia_earth/models/ipcc2019/aboveGroundBiomass.py +4 -8
  23. hestia_earth/models/ipcc2019/animal/fatContent.py +1 -1
  24. hestia_earth/models/ipcc2019/animal/milkYieldPerAnimal.py +91 -0
  25. hestia_earth/models/ipcc2019/animal/trueProteinContent.py +1 -1
  26. hestia_earth/models/ipcc2019/animal/utils.py +17 -12
  27. hestia_earth/models/ipcc2019/belowGroundBiomass.py +4 -8
  28. hestia_earth/models/ipcc2019/biomass_utils.py +11 -0
  29. hestia_earth/models/ipcc2019/ch4ToAirEntericFermentation.py +1 -1
  30. hestia_earth/models/ipcc2019/co2ToAirAboveGroundBiomassStockChange.py +8 -4
  31. hestia_earth/models/ipcc2019/co2ToAirBelowGroundBiomassStockChange.py +7 -3
  32. hestia_earth/models/ipcc2019/co2ToAirCarbonStockChange_utils.py +52 -6
  33. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChange.py +7 -3
  34. hestia_earth/models/ipcc2019/n2OToAirExcretaDirect.py +14 -9
  35. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +9 -3
  36. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1 -2
  37. hestia_earth/models/koble2014/aboveGroundCropResidue.py +5 -1
  38. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsClimateChange.py +2 -2
  39. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  40. hestia_earth/models/lcImpactAllEffects100Years/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  41. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealth.py +2 -2
  42. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthClimateChange.py +2 -2
  43. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  44. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  45. hestia_earth/models/lcImpactAllEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  46. hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  47. hestia_earth/models/lcImpactAllEffects100Years/damageToMarineEcosystemsPdfYear.py +2 -2
  48. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  49. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  50. hestia_earth/models/lcImpactAllEffects100Years/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  51. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsClimateChange.py +2 -2
  52. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  53. hestia_earth/models/lcImpactAllEffectsInfinite/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  54. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealth.py +2 -2
  55. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
  56. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  57. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  58. hestia_earth/models/lcImpactAllEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  59. hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  60. hestia_earth/models/lcImpactAllEffectsInfinite/damageToMarineEcosystemsPdfYear.py +2 -2
  61. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  62. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  63. hestia_earth/models/lcImpactAllEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  64. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  65. hestia_earth/models/lcImpactCertainEffects100Years/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  66. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealth.py +2 -2
  67. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthClimateChange.py +2 -2
  68. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  69. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  70. hestia_earth/models/lcImpactCertainEffects100Years/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  71. hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  72. hestia_earth/models/lcImpactCertainEffects100Years/damageToMarineEcosystemsPdfYear.py +2 -2
  73. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  74. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  75. hestia_earth/models/lcImpactCertainEffects100Years/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  76. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsFreshwaterEcotoxicity.py +2 -3
  77. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToFreshwaterEcosystemsPdfYear.py +2 -2
  78. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealth.py +2 -2
  79. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthClimateChange.py +2 -2
  80. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthHumanToxicityCancerogenic.py +2 -3
  81. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthHumanToxicityNonCancerogenic.py +2 -3
  82. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToHumanHealthStratosphericOzoneDepletion.py +2 -2
  83. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsMarineEcotoxicity.py +2 -3
  84. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToMarineEcosystemsPdfYear.py +2 -2
  85. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsClimateChange.py +2 -2
  86. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsPdfYear.py +2 -2
  87. hestia_earth/models/lcImpactCertainEffectsInfinite/damageToTerrestrialEcosystemsTerrestrialEcotoxicity.py +2 -3
  88. hestia_earth/models/linkedImpactAssessment/emissions.py +3 -0
  89. hestia_earth/models/log.py +4 -3
  90. hestia_earth/models/mocking/search-results.json +575 -575
  91. hestia_earth/models/pooreNemecek2018/excretaKgN.py +4 -4
  92. hestia_earth/models/pooreNemecek2018/excretaKgVs.py +4 -4
  93. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterCropResidueDecomposition.py +1 -1
  94. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterExcreta.py +1 -1
  95. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterInorganicFertiliser.py +1 -1
  96. hestia_earth/models/pooreNemecek2018/no3ToGroundwaterOrganicFertiliser.py +1 -1
  97. hestia_earth/models/pooreNemecek2018/{saplings.py → saplingsDepreciatedAmountPerCycle.py} +1 -1
  98. hestia_earth/models/pooreNemecek2018/utils.py +7 -1
  99. hestia_earth/models/recipe2016Egalitarian/damageToFreshwaterEcosystemsSpeciesYear.py +2 -2
  100. hestia_earth/models/recipe2016Egalitarian/damageToHumanHealth.py +2 -2
  101. hestia_earth/models/recipe2016Egalitarian/damageToMarineEcosystemsSpeciesYear.py +2 -2
  102. hestia_earth/models/recipe2016Egalitarian/damageToTerrestrialEcosystemsSpeciesYear.py +2 -2
  103. hestia_earth/models/recipe2016Egalitarian/freshwaterAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  104. hestia_earth/models/recipe2016Egalitarian/freshwaterEutrophicationPotential.py +2 -2
  105. hestia_earth/models/recipe2016Egalitarian/humanCarcinogenicToxicity.py +2 -3
  106. hestia_earth/models/recipe2016Egalitarian/humanNonCarcinogenicToxicity.py +2 -3
  107. hestia_earth/models/recipe2016Egalitarian/marineAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  108. hestia_earth/models/recipe2016Egalitarian/marineEutrophicationPotential.py +2 -2
  109. hestia_earth/models/recipe2016Egalitarian/ozoneDepletionPotential.py +2 -2
  110. hestia_earth/models/recipe2016Egalitarian/terrestrialAcidificationPotential.py +2 -2
  111. hestia_earth/models/recipe2016Egalitarian/terrestrialEcotoxicityPotential14Dcbeq.py +2 -3
  112. hestia_earth/models/recipe2016Hierarchist/damageToFreshwaterEcosystemsSpeciesYear.py +2 -2
  113. hestia_earth/models/recipe2016Hierarchist/damageToHumanHealth.py +2 -2
  114. hestia_earth/models/recipe2016Hierarchist/damageToMarineEcosystemsSpeciesYear.py +2 -2
  115. hestia_earth/models/recipe2016Hierarchist/damageToTerrestrialEcosystemsSpeciesYear.py +2 -2
  116. hestia_earth/models/recipe2016Hierarchist/freshwaterAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  117. hestia_earth/models/recipe2016Hierarchist/freshwaterEutrophicationPotential.py +2 -2
  118. hestia_earth/models/recipe2016Hierarchist/humanCarcinogenicToxicity.py +2 -3
  119. hestia_earth/models/recipe2016Hierarchist/humanNonCarcinogenicToxicity.py +2 -3
  120. hestia_earth/models/recipe2016Hierarchist/marineAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  121. hestia_earth/models/recipe2016Hierarchist/marineEutrophicationPotential.py +2 -2
  122. hestia_earth/models/recipe2016Hierarchist/ozoneDepletionPotential.py +2 -2
  123. hestia_earth/models/recipe2016Hierarchist/terrestrialAcidificationPotential.py +2 -2
  124. hestia_earth/models/recipe2016Hierarchist/terrestrialEcotoxicityPotential14Dcbeq.py +2 -3
  125. hestia_earth/models/recipe2016Individualist/damageToFreshwaterEcosystemsSpeciesYear.py +2 -2
  126. hestia_earth/models/recipe2016Individualist/damageToHumanHealth.py +2 -2
  127. hestia_earth/models/recipe2016Individualist/damageToMarineEcosystemsSpeciesYear.py +2 -2
  128. hestia_earth/models/recipe2016Individualist/damageToTerrestrialEcosystemsSpeciesYear.py +2 -2
  129. hestia_earth/models/recipe2016Individualist/freshwaterAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  130. hestia_earth/models/recipe2016Individualist/freshwaterEutrophicationPotential.py +2 -2
  131. hestia_earth/models/recipe2016Individualist/humanCarcinogenicToxicity.py +2 -3
  132. hestia_earth/models/recipe2016Individualist/humanNonCarcinogenicToxicity.py +2 -3
  133. hestia_earth/models/recipe2016Individualist/marineAquaticEcotoxicityPotential14Dcbeq.py +2 -3
  134. hestia_earth/models/recipe2016Individualist/marineEutrophicationPotential.py +2 -2
  135. hestia_earth/models/recipe2016Individualist/ozoneDepletionPotential.py +2 -2
  136. hestia_earth/models/recipe2016Individualist/terrestrialAcidificationPotential.py +2 -2
  137. hestia_earth/models/recipe2016Individualist/terrestrialEcotoxicityPotential14Dcbeq.py +2 -3
  138. hestia_earth/models/site/management.py +142 -144
  139. hestia_earth/models/stehfestBouwman2006/n2OToAirCropResidueDecompositionDirect.py +1 -1
  140. hestia_earth/models/stehfestBouwman2006/n2OToAirExcretaDirect.py +1 -1
  141. hestia_earth/models/stehfestBouwman2006/n2OToAirInorganicFertiliserDirect.py +1 -1
  142. hestia_earth/models/stehfestBouwman2006/n2OToAirOrganicFertiliserDirect.py +1 -1
  143. hestia_earth/models/stehfestBouwman2006/noxToAirCropResidueDecomposition.py +1 -1
  144. hestia_earth/models/stehfestBouwman2006/noxToAirExcreta.py +1 -1
  145. hestia_earth/models/stehfestBouwman2006/noxToAirInorganicFertiliser.py +1 -1
  146. hestia_earth/models/stehfestBouwman2006/noxToAirOrganicFertiliser.py +1 -1
  147. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirCropResidueDecomposition.py +1 -1
  148. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirExcreta.py +1 -1
  149. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirInorganicFertiliser.py +1 -1
  150. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirOrganicFertiliser.py +1 -1
  151. hestia_earth/models/usetoxV2/freshwaterEcotoxicityPotentialCtue.py +2 -3
  152. hestia_earth/models/utils/__init__.py +4 -1
  153. hestia_earth/models/utils/blank_node.py +34 -14
  154. hestia_earth/models/utils/emission.py +1 -8
  155. hestia_earth/models/utils/lookup.py +2 -1
  156. hestia_earth/models/utils/management.py +11 -0
  157. hestia_earth/models/utils/pesticideAI.py +11 -17
  158. hestia_earth/models/utils/term.py +2 -1
  159. hestia_earth/models/version.py +1 -1
  160. {hestia_earth_models-0.64.11.dist-info → hestia_earth_models-0.64.13.dist-info}/METADATA +4 -4
  161. {hestia_earth_models-0.64.11.dist-info → hestia_earth_models-0.64.13.dist-info}/RECORD +192 -188
  162. {hestia_earth_models-0.64.11.dist-info → hestia_earth_models-0.64.13.dist-info}/WHEEL +1 -1
  163. tests/models/cycle/completeness/test_seed.py +1 -1
  164. tests/models/cycle/test_endDate.py +18 -2
  165. tests/models/cycle/test_startDate.py +21 -3
  166. tests/models/faostat2018/test_faostat_utils.py +84 -0
  167. tests/models/hestia/__init__.py +0 -0
  168. tests/models/hestia/test_landCover.py +210 -0
  169. tests/models/ipcc2019/animal/test_milkYieldPerAnimal.py +21 -0
  170. tests/models/ipcc2019/test_aboveGroundBiomass.py +2 -1
  171. tests/models/ipcc2019/test_belowGroundBiomass.py +2 -1
  172. tests/models/ipcc2019/test_co2ToAirAboveGroundBiomassStockChange.py +4 -3
  173. tests/models/ipcc2019/test_co2ToAirBelowGroundBiomassStockChange.py +3 -3
  174. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChange.py +49 -2
  175. tests/models/ipcc2019/test_n2OToAirExcretaDirect.py +12 -0
  176. tests/models/ipcc2019/test_organicCarbonPerHa.py +1 -0
  177. tests/models/koble2014/test_aboveGroundCropResidue.py +13 -0
  178. tests/models/pooreNemecek2018/test_no3ToGroundwaterCropResidueDecomposition.py +3 -2
  179. tests/models/pooreNemecek2018/test_no3ToGroundwaterExcreta.py +3 -2
  180. tests/models/pooreNemecek2018/test_no3ToGroundwaterInorganicFertiliser.py +3 -2
  181. tests/models/pooreNemecek2018/test_no3ToGroundwaterOrganicFertiliser.py +3 -2
  182. tests/models/pooreNemecek2018/{test_saplings.py → test_saplingsDepreciatedAmountPerCycle.py} +1 -1
  183. tests/models/site/test_management.py +18 -151
  184. tests/models/utils/test_blank_node.py +57 -1
  185. tests/models/utils/test_emission.py +1 -6
  186. tests/models/utils/test_site.py +33 -2
  187. tests/models/pooreNemecek2018/test_no3ToGroundwaterSoilFlux.py +0 -90
  188. tests/models/stehfestBouwman2006/test_n2OToAirSoilFlux.py +0 -41
  189. tests/models/stehfestBouwman2006/test_noxToAirSoilFlux.py +0 -40
  190. tests/models/stehfestBouwman2006GisImplementation/test_noxToAirSoilFlux.py +0 -33
  191. /hestia_earth/models/pooreNemecek2018/{no3ToGroundwaterSoilFlux.py → no3ToGroundwaterSoilFlux_utils.py} +0 -0
  192. /hestia_earth/models/stehfestBouwman2006/{n2OToAirSoilFlux.py → n2OToAirSoilFlux_utils.py} +0 -0
  193. /hestia_earth/models/stehfestBouwman2006/{noxToAirSoilFlux.py → noxToAirSoilFlux_utils.py} +0 -0
  194. /hestia_earth/models/stehfestBouwman2006GisImplementation/{noxToAirSoilFlux.py → noxToAirSoilFlux_utils.py} +0 -0
  195. {hestia_earth_models-0.64.11.dist-info → hestia_earth_models-0.64.13.dist-info}/LICENSE +0 -0
  196. {hestia_earth_models-0.64.11.dist-info → hestia_earth_models-0.64.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,727 @@
1
+ """
2
+ Land Cover
3
+
4
+ This model calculates historic land use change over a twenty-year period, extending the
5
+ functionality of the Blonk model.
6
+ """
7
+ import math
8
+ from collections import defaultdict
9
+ from hestia_earth.schema import SiteSiteType, TermTermType
10
+ from hestia_earth.utils.lookup import (
11
+ download_lookup, get_table_value, column_name,
12
+ extract_grouped_data_closest_date, _is_missing_value, extract_grouped_data
13
+ )
14
+ from hestia_earth.utils.model import filter_list_term_type
15
+ from hestia_earth.utils.tools import safe_parse_float, to_precision, non_empty_value
16
+
17
+ from hestia_earth.models.log import logRequirements, log_as_table, logShouldRun
18
+ from hestia_earth.models.utils.management import _new_management
19
+ from hestia_earth.models.utils.term import get_lookup_value
20
+ from . import MODEL
21
+
22
+ REQUIREMENTS = {
23
+ "Site": {
24
+ "siteType": [
25
+ "forest",
26
+ "cropland",
27
+ "permanent pasture",
28
+ "other natural vegetation"
29
+ ],
30
+ "country": "",
31
+ "management": [
32
+ {
33
+ "@type": "Management",
34
+ "value": "",
35
+ "term.termType": "landCover",
36
+ "endDate": ""
37
+ }
38
+ ]
39
+ }
40
+ }
41
+ RETURNS = {
42
+ "Management": [{
43
+ "@type": "Management",
44
+ "term.termType": "landCover",
45
+ "term.@id": [
46
+ "Forest", "Annual cropland", "Permanent cropland", "Permanent pasture", "Other natural vegetation"
47
+ ],
48
+ "value": "",
49
+ "endDate": "",
50
+ "startDate": ""
51
+ }]
52
+ }
53
+ LOOKUPS = {
54
+ "region-crop-cropGroupingFaostatProduction-areaHarvestedUpTo20YearExpansion": "All crops",
55
+ "region-crop-cropGroupingFaostatProduction-areaHarvested": "All crops",
56
+ "region-faostatArea-UpTo20YearExpansion": "All land uses",
57
+ "region-faostatArea": [
58
+ "Arable land",
59
+ "Cropland",
60
+ "Forest land",
61
+ "Land area",
62
+ "Other land",
63
+ "Permanent crops",
64
+ "Permanent meadows and pastures"
65
+ ],
66
+ "crop": ["cropGroupingFaostatArea", "IPCC_LAND_USE_CATEGORY"],
67
+ "landCover": ["cropGroupingFaostatProduction", "FAOSTAT_LAND_AREA_CATEGORY"]
68
+ }
69
+ MODEL_KEY = 'landCover'
70
+
71
+ LAND_AREA = LOOKUPS["region-faostatArea"][3]
72
+ TOTAL_CROPLAND = "Cropland"
73
+ ANNUAL_CROPLAND = "Arable land"
74
+ FOREST_LAND = "Forest land"
75
+ OTHER_LAND = "Other land"
76
+ PERMANENT_CROPLAND = "Permanent crops"
77
+ PERMANENT_PASTURE = "Permanent meadows and pastures"
78
+ TOTAL_AGRICULTURAL_CHANGE = "Total agricultural change"
79
+ ALL_LAND_USE_TERMS = [
80
+ FOREST_LAND,
81
+ TOTAL_CROPLAND,
82
+ ANNUAL_CROPLAND,
83
+ PERMANENT_CROPLAND,
84
+ PERMANENT_PASTURE,
85
+ OTHER_LAND
86
+ ]
87
+ # Mapping from Land use terms to Management node terms.
88
+ # land use term: (@id, name)
89
+ LAND_USE_TERMS_FOR_TRANSFORMATION = {
90
+ FOREST_LAND: ("forest", "Forest"),
91
+ ANNUAL_CROPLAND: ("annualCropland", "Annual cropland"),
92
+ PERMANENT_CROPLAND: ("permanentCropland", "Permanent cropland"),
93
+ PERMANENT_PASTURE: ("permanentPasture", "Permanent pasture"),
94
+ OTHER_LAND: ("otherLand", OTHER_LAND) # Not used yet
95
+ }
96
+ SITE_TYPES = {
97
+ SiteSiteType.CROPLAND.value,
98
+ SiteSiteType.FOREST.value,
99
+ SiteSiteType.OTHER_NATURAL_VEGETATION.value,
100
+ SiteSiteType.PERMANENT_PASTURE.value
101
+ }
102
+ DEFAULT_WINDOW_IN_YEARS = 20
103
+ IPCC_LAND_USE_CATEGORY_ANNUAL = "Annual crops"
104
+ IPCC_LAND_USE_CATEGORY_PERENNIAL = "Perennial crops"
105
+ OUTPUT_SIGNIFICANT_DIGITS = 3
106
+
107
+
108
+ def _management(term_id: str, value: float, start_date: str, end_date: str):
109
+ node = _new_management(term_id, MODEL)
110
+ node['value'] = value
111
+ node['startDate'] = start_date
112
+ node['endDate'] = end_date
113
+ return node
114
+
115
+
116
+ def _is_missing_or_none(value) -> bool:
117
+ return value is None or _is_missing_value(value)
118
+
119
+
120
+ def _safe_divide(numerator, denominator, default=0) -> float:
121
+ return default if not denominator else numerator / denominator
122
+
123
+
124
+ def site_area_sum_to_100(dict_of_percentages: dict):
125
+ return False if dict_of_percentages == {} else \
126
+ (math.isclose(sum(dict_of_percentages.values()), 1.0, rel_tol=0.01) or
127
+ math.isclose(sum(dict_of_percentages.values()), 0.0, rel_tol=0.01))
128
+
129
+
130
+ def _lookup_land_use_type(nodes: list) -> str:
131
+ """Look up the land use type from a management node."""
132
+ return "" if nodes == [] else get_lookup_value(
133
+ lookup_term=nodes[0].get("term", {}),
134
+ column=LOOKUPS.get("landCover")[1],
135
+ model=MODEL,
136
+ term=nodes[0].get("term", {})
137
+ )
138
+
139
+
140
+ def _crop_ipcc_land_use_category(
141
+ crop_term_id: str,
142
+ lookup_term_type: str = TermTermType.LANDCOVER.value
143
+ ) -> str:
144
+ """
145
+ Looks up the crop in the lookup.
146
+ Returns the IPCC_LAND_USE_CATEGORY.
147
+ """
148
+ return get_lookup_value(
149
+ lookup_term={"@id": crop_term_id, "type": "Term", "termType": lookup_term_type},
150
+ column=LOOKUPS.get("crop")[1],
151
+ model=MODEL,
152
+ term={"@id": crop_term_id, "type": "Term", "termType": lookup_term_type}
153
+ )
154
+
155
+
156
+ def get_changes(country_id: str, end_year: int) -> dict:
157
+ """
158
+ For each entry in ALL_LAND_USE_TERMS, creates a key: value in output dictionary, also TOTAL
159
+ """
160
+ lookup = download_lookup("region-faostatArea-UpTo20YearExpansion.csv")
161
+ changes_dict = {
162
+ land_use_term: safe_parse_float(
163
+ extract_grouped_data(
164
+ get_table_value(lookup, 'termid', country_id, column_name(land_use_term)),
165
+ str(end_year))
166
+ )
167
+ for land_use_term in ALL_LAND_USE_TERMS + [LAND_AREA]
168
+ }
169
+ changes_dict[TOTAL_AGRICULTURAL_CHANGE] = (float(changes_dict.get(TOTAL_CROPLAND, 0))
170
+ + float(changes_dict.get(PERMANENT_PASTURE, 0)))
171
+
172
+ return changes_dict
173
+
174
+
175
+ def _get_ratio_start_and_end_values(
176
+ expansion: float,
177
+ fao_name: str,
178
+ country_id: str,
179
+ end_year: int
180
+ ) -> float:
181
+ # expansion over twenty years / current area
182
+ lookup = download_lookup('region-faostatArea.csv')
183
+ table_value = get_table_value(lookup, 'termid', country_id, column_name(fao_name))
184
+ end_value = safe_parse_float(value=extract_grouped_data_closest_date(table_value, end_year), default=None)
185
+ return max(0.0, _safe_divide(numerator=expansion, denominator=end_value))
186
+
187
+
188
+ def _estimate_maximum_forest_change(
189
+ forest_change: float, total_cropland_change: float, pasture_change: float, total_agricultural_change: float
190
+ ):
191
+ """
192
+ (L): Estimate maximum forest loss
193
+ Gives a negative number representing forest loss. Does not currently handle forest gain.
194
+ """
195
+ positive_change = pasture_change > 0 and total_cropland_change > 0
196
+ return _negative_agricultural_land_change(
197
+ forest_change=forest_change,
198
+ pasture_change=pasture_change,
199
+ total_cropland_change=total_cropland_change
200
+ ) if not positive_change else (
201
+ total_agricultural_change
202
+ if -min(forest_change, 0) > total_agricultural_change else
203
+ min(forest_change, 0)
204
+ )
205
+
206
+
207
+ def _negative_agricultural_land_change(forest_change, pasture_change, total_cropland_change):
208
+ return pasture_change if 0 < pasture_change < -min(forest_change, 0) \
209
+ else min(forest_change, 0) if pasture_change > 0 \
210
+ else -total_cropland_change if 0 < total_cropland_change < -min(forest_change, 0) \
211
+ else min(forest_change, 0) if 0 < total_cropland_change \
212
+ else 0
213
+
214
+
215
+ def _allocate_forest_loss(forest_loss: float, changes: dict):
216
+ """Allocate forest loss between agricultural categories for the specific country"""
217
+ return {
218
+ TOTAL_CROPLAND: forest_loss * _safe_divide(
219
+ numerator=max(changes[TOTAL_CROPLAND], 0),
220
+ denominator=max(changes[TOTAL_CROPLAND], 0) + max(changes[PERMANENT_PASTURE], 0)
221
+ ),
222
+ PERMANENT_PASTURE: forest_loss * _safe_divide(
223
+ numerator=max(changes[PERMANENT_PASTURE], 0),
224
+ denominator=max(changes[TOTAL_CROPLAND], 0) + max(changes[PERMANENT_PASTURE], 0)
225
+ )
226
+ }
227
+
228
+
229
+ def _additional_allocation(changes, max_forest_loss_to_cropland, max_forest_loss_to_permanent_pasture):
230
+ """Determine how much area still needs to be assigned"""
231
+ return {
232
+ TOTAL_CROPLAND: max(changes[TOTAL_CROPLAND], 0) + max_forest_loss_to_cropland,
233
+ PERMANENT_PASTURE: max(changes[PERMANENT_PASTURE], 0) + max_forest_loss_to_permanent_pasture
234
+ }
235
+
236
+
237
+ def _allocate_cropland_loss_to_pasture(changes: dict, land_required_for_permanent_pasture: float):
238
+ """Allocate changes between Permanent pasture and cropland"""
239
+ return (
240
+ max(-land_required_for_permanent_pasture, changes[TOTAL_CROPLAND])
241
+ if changes[TOTAL_CROPLAND] < 0 else 0
242
+ )
243
+
244
+
245
+ def _allocate_pasture_loss_to_cropland(changes: dict, land_required_for_cropland: float):
246
+ """Allocate changes between Permanent pasture and cropland"""
247
+ return (
248
+ max(-land_required_for_cropland, changes[PERMANENT_PASTURE])
249
+ if changes[PERMANENT_PASTURE] < 0 else 0
250
+ )
251
+
252
+
253
+ def _allocate_other_land(
254
+ changes: dict, max_forest_loss_to: dict, pasture_loss_to_cropland: float, cropland_loss_to_pasture: float
255
+ ) -> dict:
256
+ """Allocate changes between Other land and cropland"""
257
+ other_land_loss_to_cropland = (
258
+ -(max(changes[TOTAL_CROPLAND], 0) + max_forest_loss_to[TOTAL_CROPLAND]
259
+ + pasture_loss_to_cropland)
260
+ )
261
+ other_land_loss_to_pasture = (
262
+ -(max(changes[PERMANENT_PASTURE], 0) + max_forest_loss_to[PERMANENT_PASTURE]
263
+ + cropland_loss_to_pasture)
264
+ )
265
+ return {
266
+ TOTAL_CROPLAND: other_land_loss_to_cropland,
267
+ PERMANENT_PASTURE: other_land_loss_to_pasture,
268
+ TOTAL_AGRICULTURAL_CHANGE: other_land_loss_to_cropland + other_land_loss_to_pasture
269
+ }
270
+
271
+
272
+ def _allocate_annual_permanent_cropland_losses(changes: dict) -> tuple:
273
+ """
274
+ (Z, AA): Allocate changes between Annual cropland and Permanent cropland
275
+ Returns: annual_cropland_loss_to_permanent_cropland, permanent_cropland_loss_to_annual_cropland
276
+ """
277
+ return (
278
+ -min(-changes[ANNUAL_CROPLAND], changes[PERMANENT_CROPLAND])
279
+ if (changes[ANNUAL_CROPLAND] < 0 and changes[PERMANENT_CROPLAND] > 0) else 0,
280
+ -min(changes[ANNUAL_CROPLAND], -changes[PERMANENT_CROPLAND])
281
+ if (changes[ANNUAL_CROPLAND] > 0 and changes[PERMANENT_CROPLAND] < 0) else 0
282
+ )
283
+
284
+
285
+ def _estimate_conversions_to_annual_cropland(
286
+ changes: dict,
287
+ pasture_loss_to_crops: float,
288
+ forest_loss_to_cropland: float,
289
+ other_land_loss_to_annual_cropland: float,
290
+ permanent_to_annual_cropland: float
291
+ ) -> dict:
292
+ """(AC-AG): Estimate percentage of land sources when converted to: Annual cropland"""
293
+ # -> percent_annual_cropland_was[]
294
+ def conversion_to_annual_cropland(factor: float):
295
+ return factor * _safe_divide(
296
+ numerator=_safe_divide(
297
+ numerator=max(changes[ANNUAL_CROPLAND], 0),
298
+ denominator=max(changes[ANNUAL_CROPLAND], 0) + max(changes[PERMANENT_CROPLAND], 0)),
299
+ denominator=-changes[ANNUAL_CROPLAND]
300
+ )
301
+
302
+ percentages = {
303
+ FOREST_LAND: conversion_to_annual_cropland(forest_loss_to_cropland),
304
+ OTHER_LAND: conversion_to_annual_cropland(other_land_loss_to_annual_cropland),
305
+ PERMANENT_PASTURE: conversion_to_annual_cropland(pasture_loss_to_crops),
306
+ PERMANENT_CROPLAND: _safe_divide(numerator=permanent_to_annual_cropland, denominator=-changes[ANNUAL_CROPLAND])
307
+ }
308
+ return percentages
309
+
310
+
311
+ def _estimate_conversions_to_permanent_cropland(
312
+ changes: dict,
313
+ annual_loss_to_permanent_cropland: float,
314
+ pasture_loss_to_cropland: float,
315
+ forest_loss_to_cropland: float,
316
+ other_land_loss_to_annual_cropland: float
317
+ ) -> dict:
318
+ """Estimate percentage of land sources when converted to: Annual cropland"""
319
+ def conversion_to_permanent_cropland(factor: float):
320
+ return _safe_divide(
321
+ numerator=_safe_divide(
322
+ numerator=factor * max(changes[PERMANENT_CROPLAND], 0),
323
+ denominator=max(changes[ANNUAL_CROPLAND], 0) + max(changes[PERMANENT_CROPLAND], 0)),
324
+ denominator=-changes[PERMANENT_CROPLAND]
325
+ )
326
+
327
+ percentages = {
328
+ FOREST_LAND: conversion_to_permanent_cropland(forest_loss_to_cropland),
329
+ OTHER_LAND: conversion_to_permanent_cropland(other_land_loss_to_annual_cropland),
330
+ PERMANENT_PASTURE: conversion_to_permanent_cropland(pasture_loss_to_cropland),
331
+ ANNUAL_CROPLAND: conversion_to_permanent_cropland(annual_loss_to_permanent_cropland)
332
+ }
333
+ return percentages
334
+
335
+
336
+ def _estimate_conversions_to_pasture(
337
+ changes: dict,
338
+ forest_loss_to_pasture: float,
339
+ total_cropland_loss_to_pasture: float,
340
+ other_land_loss_to_pasture: float
341
+ ) -> dict:
342
+ """Estimate percentage of land sources when converted to: Permanent pasture"""
343
+ percentages = {
344
+ FOREST_LAND: _safe_divide(
345
+ numerator=forest_loss_to_pasture,
346
+ denominator=-changes[PERMANENT_PASTURE],
347
+ ),
348
+ OTHER_LAND: _safe_divide(
349
+ numerator=other_land_loss_to_pasture,
350
+ denominator=-changes[PERMANENT_PASTURE]
351
+ ),
352
+ # AT
353
+ ANNUAL_CROPLAND: _safe_divide(
354
+ numerator=total_cropland_loss_to_pasture * _safe_divide(
355
+ numerator=min(changes[ANNUAL_CROPLAND], 0),
356
+ denominator=(min(changes[ANNUAL_CROPLAND], 0) + min(changes[PERMANENT_CROPLAND], 0))
357
+ ),
358
+ denominator=-changes[PERMANENT_PASTURE]
359
+ ),
360
+ PERMANENT_CROPLAND: _safe_divide(
361
+ numerator=total_cropland_loss_to_pasture * _safe_divide(
362
+ numerator=min(changes[PERMANENT_CROPLAND], 0),
363
+ denominator=(min(changes[ANNUAL_CROPLAND], 0) + min(changes[PERMANENT_CROPLAND], 0))
364
+ ),
365
+ denominator=-changes[PERMANENT_PASTURE]
366
+ )
367
+ }
368
+ return percentages
369
+
370
+
371
+ def _get_shares_of_expansion(
372
+ land_use_type: str,
373
+ percent_annual_cropland_was: dict,
374
+ percent_permanent_cropland_was: dict,
375
+ percent_pasture_was: dict
376
+ ) -> dict:
377
+ expansion_for_type = {
378
+ ANNUAL_CROPLAND: percent_annual_cropland_was,
379
+ PERMANENT_CROPLAND: percent_permanent_cropland_was,
380
+ PERMANENT_PASTURE: percent_pasture_was
381
+ }
382
+ return {
383
+ k: expansion_for_type[land_use_type].get(k, 0)
384
+ for k in LAND_USE_TERMS_FOR_TRANSFORMATION.keys()
385
+ }
386
+
387
+
388
+ def _get_faostat_name(term: dict) -> str:
389
+ """For landCover terms, find the cropGroupingFaostatArea name for the landCover id."""
390
+ return get_lookup_value(term, "cropGroupingFaostatArea")
391
+
392
+
393
+ def _get_complete_faostat_to_crop_mapping() -> dict:
394
+ """Returns mapping in the format: {faostat_name: IPPC_LAND_USE_CATEGORY, ...}"""
395
+ lookup = download_lookup("crop.csv")
396
+ mappings = defaultdict(list)
397
+ for crop_term_id in [row[0] for row in lookup]:
398
+ key = column_name(
399
+ get_table_value(lookup, 'termid', crop_term_id, column_name("cropGroupingFaostatArea"))
400
+ )
401
+ if key:
402
+ mappings[key].append(_crop_ipcc_land_use_category(crop_term_id=crop_term_id, lookup_term_type="crop"))
403
+ return {
404
+ fao_name: max(set(crop_terms), key=crop_terms.count)
405
+ for fao_name, crop_terms in mappings.items()
406
+ }
407
+
408
+
409
+ def _get_harvested_area(country_id: str, year: int, faostat_name: str) -> float:
410
+ """
411
+ Returns a dictionary of harvested areas for the country & year, indexed by landCover term (crop)
412
+ """
413
+ lookup = download_lookup("region-crop-cropGroupingFaostatProduction-areaHarvested.csv")
414
+ return safe_parse_float(
415
+ value=extract_grouped_data_closest_date(
416
+ data=get_table_value(lookup, "termid", country_id, column_name(faostat_name)),
417
+ year=year
418
+ ),
419
+ default=None
420
+ )
421
+
422
+
423
+ def _run_make_management_nodes(existing_nodes: list, percentage_transformed_from: dict, start_year: int) -> list:
424
+ """Creates a list of new management nodes, excluding any dates matching existing ones."""
425
+ existing_nodes_set = {
426
+ (node.get("term", {}).get("@id", ""), node.get("startDate"), node.get("endDate"))
427
+ for node in existing_nodes
428
+ }
429
+ values = [
430
+ {
431
+ "land_management_key": (
432
+ LAND_USE_TERMS_FOR_TRANSFORMATION[land_type], f"{start_year}-01-01", f"{start_year}-12-31"
433
+ ),
434
+ "land_type": land_type,
435
+ "percentage": 0.0 if ratio == -0.0 else to_precision(
436
+ number=ratio * 100,
437
+ digits=OUTPUT_SIGNIFICANT_DIGITS
438
+ )
439
+ }
440
+ for land_type, ratio in percentage_transformed_from.items()
441
+ ]
442
+ values = [v for v in values if v.get("land_management_key") not in existing_nodes_set]
443
+
444
+ return [
445
+ _management(
446
+ term_id=LAND_USE_TERMS_FOR_TRANSFORMATION[value.get("land_type")][0],
447
+ value=value.get("percentage"),
448
+ start_date=value.get("land_management_key")[1],
449
+ end_date=value.get("land_management_key")[2]
450
+ )
451
+ for value in values
452
+ ]
453
+
454
+
455
+ def get_ratio_of_expanded_area(country_id: str, fao_name: str, end_year: int) -> float:
456
+ lookup = download_lookup("region-crop-cropGroupingFaostatProduction-areaHarvestedUpTo20YearExpansion.csv")
457
+ table_value = get_table_value(lookup, 'termid', country_id, column_name(fao_name))
458
+ expansion = safe_parse_float(value=extract_grouped_data(table_value, str(end_year)), default=None)
459
+ end_value = _get_harvested_area(
460
+ country_id=country_id,
461
+ year=end_year,
462
+ faostat_name=fao_name
463
+ )
464
+ return 0.0 if any([expansion is None, end_value is None]) else max(
465
+ 0.0, _safe_divide(numerator=expansion, denominator=(end_value - expansion))
466
+ )
467
+
468
+
469
+ def _get_sum_for_land_category(
470
+ values: dict,
471
+ year: int,
472
+ ipcc_land_use_category,
473
+ fao_stat_to_ipcc_type: dict,
474
+ include_negatives: bool = True
475
+ ) -> float:
476
+ return sum(
477
+ [
478
+ safe_parse_float(value=extract_grouped_data(table_value, str(year)), default=None)
479
+ for fao_name, table_value in values.items()
480
+ if not _is_missing_or_none(extract_grouped_data(table_value, str(year))) and
481
+ fao_stat_to_ipcc_type[fao_name] == ipcc_land_use_category and
482
+ (include_negatives or
483
+ safe_parse_float(value=extract_grouped_data(table_value, str(year)), default=None) > 0.0)
484
+ ]
485
+ )
486
+
487
+
488
+ def _get_sums_of_crop_expansion(country_id: str, year: int, include_negatives: bool = True) -> tuple[float, float]:
489
+ """
490
+ Sum net expansion for all annual and permanent crops, returned as two values.
491
+ Returns a tuple of (expansion of annual crops, expansion of permanent crops)
492
+ """
493
+ lookup = download_lookup("region-crop-cropGroupingFaostatProduction-areaHarvestedUpTo20YearExpansion.csv")
494
+ values = {name: get_table_value(lookup, 'termid', country_id, column_name(name))
495
+ for name in list(lookup.dtype.names) if name != "termid"}
496
+
497
+ fao_stat_to_ipcc_type = _get_complete_faostat_to_crop_mapping()
498
+
499
+ annual_sum_of_expansion = _get_sum_for_land_category(
500
+ values=values,
501
+ year=year,
502
+ ipcc_land_use_category=IPCC_LAND_USE_CATEGORY_ANNUAL,
503
+ fao_stat_to_ipcc_type=fao_stat_to_ipcc_type,
504
+ include_negatives=include_negatives
505
+ )
506
+ permanent_sum_of_expansion = _get_sum_for_land_category(
507
+ values=values,
508
+ year=year,
509
+ ipcc_land_use_category=IPCC_LAND_USE_CATEGORY_PERENNIAL,
510
+ fao_stat_to_ipcc_type=fao_stat_to_ipcc_type,
511
+ include_negatives=include_negatives
512
+ )
513
+
514
+ return annual_sum_of_expansion, permanent_sum_of_expansion
515
+
516
+
517
+ def _get_net_expansion_cultivated_vs_harvested(annual_crops_net_expansion, changes, land_use_type,
518
+ permanent_crops_net_expansion):
519
+ if land_use_type == ANNUAL_CROPLAND:
520
+ net_expansion_cultivated_vs_harvested = _safe_divide(numerator=max(0, changes[ANNUAL_CROPLAND]),
521
+ denominator=(annual_crops_net_expansion / 1000))
522
+ elif land_use_type == PERMANENT_CROPLAND:
523
+ net_expansion_cultivated_vs_harvested = _safe_divide(numerator=max(0, changes[PERMANENT_CROPLAND]),
524
+ denominator=(permanent_crops_net_expansion / 1000))
525
+ else:
526
+ net_expansion_cultivated_vs_harvested = 1
527
+ return net_expansion_cultivated_vs_harvested
528
+
529
+
530
+ def _should_run_historical_land_use_change(site: dict, land_use_type: str) -> tuple[bool, dict]:
531
+ management_nodes = filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
532
+ # Assume a single management node for single-cropping.
533
+ return _should_run_historical_land_use_change_single_crop(
534
+ site=site,
535
+ term=management_nodes[0].get("term", {}),
536
+ country_id=site.get("country", {}).get("@id"),
537
+ end_year=int(management_nodes[0].get("endDate")[:4]),
538
+ land_use_type=land_use_type
539
+ )
540
+
541
+
542
+ def _should_run_historical_land_use_change_single_crop(
543
+ site: dict,
544
+ term: dict,
545
+ country_id: str,
546
+ end_year: int,
547
+ land_use_type: str
548
+ ) -> tuple[bool, dict]:
549
+ """Calculate land use change percentages for a single management node/crop."""
550
+ # (C-H).
551
+ changes = get_changes(country_id=country_id, end_year=end_year)
552
+
553
+ # (L). Estimate maximum forest loss
554
+ forest_loss = _estimate_maximum_forest_change(
555
+ forest_change=changes[FOREST_LAND],
556
+ total_cropland_change=changes[TOTAL_CROPLAND],
557
+ pasture_change=changes[PERMANENT_PASTURE],
558
+ total_agricultural_change=changes[TOTAL_AGRICULTURAL_CHANGE]
559
+ )
560
+
561
+ # (M, N). Allocate forest loss between agricultural categories for the specific country
562
+ forest_loss_to = _allocate_forest_loss(forest_loss=forest_loss, changes=changes)
563
+
564
+ # (P, Q): Determine how much area still needs to be assigned
565
+ land_required_for = _additional_allocation(
566
+ changes=changes,
567
+ max_forest_loss_to_cropland=forest_loss_to[TOTAL_CROPLAND],
568
+ max_forest_loss_to_permanent_pasture=forest_loss_to[PERMANENT_PASTURE]
569
+ )
570
+
571
+ # (R): Allocate changes between Permanent pasture and cropland
572
+ cropland_loss_to_pasture = _allocate_cropland_loss_to_pasture(
573
+ changes=changes,
574
+ land_required_for_permanent_pasture=land_required_for[PERMANENT_PASTURE]
575
+ )
576
+ # (S)
577
+ pasture_loss_to_cropland = _allocate_pasture_loss_to_cropland(
578
+ changes=changes,
579
+ land_required_for_cropland=land_required_for[TOTAL_CROPLAND]
580
+ )
581
+
582
+ # (V): Allocate changes between Other land and cropland
583
+ other_land_loss_to = _allocate_other_land(
584
+ changes=changes,
585
+ max_forest_loss_to=forest_loss_to,
586
+ pasture_loss_to_cropland=pasture_loss_to_cropland,
587
+ cropland_loss_to_pasture=cropland_loss_to_pasture
588
+ )
589
+
590
+ # (Z, AA): Allocate changes between Annual cropland and Permanent cropland
591
+ annual_cropland_loss_to_permanent_cropland, permanent_cropland_loss_to_annual_cropland = (
592
+ _allocate_annual_permanent_cropland_losses(changes)
593
+ )
594
+
595
+ # (AC-AG): Estimate percentage of land sources when converted to: Annual cropland
596
+ # Note: All percentages are expressed as decimal fractions. 50% = 0.5
597
+ percent_annual_cropland_was = _estimate_conversions_to_annual_cropland(
598
+ changes=changes,
599
+ pasture_loss_to_crops=pasture_loss_to_cropland,
600
+ forest_loss_to_cropland=forest_loss_to[TOTAL_CROPLAND],
601
+ other_land_loss_to_annual_cropland=other_land_loss_to[TOTAL_CROPLAND],
602
+ permanent_to_annual_cropland=permanent_cropland_loss_to_annual_cropland,
603
+ )
604
+
605
+ # (AJ-AM): Estimate percentage of land sources when converted to: Permanent cropland
606
+ percent_permanent_cropland_was = _estimate_conversions_to_permanent_cropland(
607
+ changes=changes,
608
+ annual_loss_to_permanent_cropland=annual_cropland_loss_to_permanent_cropland,
609
+ pasture_loss_to_cropland=pasture_loss_to_cropland,
610
+ forest_loss_to_cropland=forest_loss_to[TOTAL_CROPLAND],
611
+ other_land_loss_to_annual_cropland=other_land_loss_to[TOTAL_CROPLAND]
612
+ )
613
+
614
+ # Estimate percentage of land sources when converted to: Permanent pasture
615
+ percent_pasture_was = _estimate_conversions_to_pasture(
616
+ changes=changes,
617
+ forest_loss_to_pasture=forest_loss_to[PERMANENT_PASTURE],
618
+ total_cropland_loss_to_pasture=cropland_loss_to_pasture,
619
+ other_land_loss_to_pasture=other_land_loss_to[PERMANENT_PASTURE]
620
+ )
621
+
622
+ # BA to BD
623
+ shares_of_expansion = _get_shares_of_expansion(
624
+ land_use_type=land_use_type,
625
+ percent_annual_cropland_was=percent_annual_cropland_was,
626
+ percent_permanent_cropland_was=percent_permanent_cropland_was,
627
+ percent_pasture_was=percent_pasture_was
628
+ )
629
+
630
+ # Cell E8
631
+ expansion_factor = _get_ratio_start_and_end_values(
632
+ expansion=changes[PERMANENT_PASTURE],
633
+ fao_name=PERMANENT_PASTURE,
634
+ country_id=country_id,
635
+ end_year=end_year
636
+ ) if land_use_type == PERMANENT_PASTURE else get_ratio_of_expanded_area(
637
+ country_id=country_id,
638
+ fao_name=_get_faostat_name(term),
639
+ end_year=end_year
640
+ )
641
+
642
+ # E9
643
+ annual_crops_net_expansion, permanent_crops_net_expansion = _get_sums_of_crop_expansion(
644
+ country_id=country_id,
645
+ year=end_year,
646
+ include_negatives=True
647
+ )
648
+ annual_crops_gross_expansion, permanent_crops_gross_expansion = _get_sums_of_crop_expansion(
649
+ country_id=country_id,
650
+ year=end_year,
651
+ include_negatives=False
652
+ )
653
+ e9_net_expansion = _safe_divide(
654
+ numerator=permanent_crops_net_expansion,
655
+ denominator=permanent_crops_gross_expansion
656
+ ) if land_use_type == PERMANENT_CROPLAND else (
657
+ _safe_divide(
658
+ numerator=annual_crops_net_expansion,
659
+ denominator=annual_crops_gross_expansion
660
+ ) if land_use_type == ANNUAL_CROPLAND else 1
661
+ )
662
+
663
+ # E10: Compare changes to annual/perennial cropland from net expansion.
664
+ net_expansion_cultivated_vs_harvested = _get_net_expansion_cultivated_vs_harvested(
665
+ annual_crops_net_expansion=annual_crops_net_expansion,
666
+ changes=changes,
667
+ land_use_type=land_use_type,
668
+ permanent_crops_net_expansion=permanent_crops_net_expansion
669
+ )
670
+
671
+ site_area = {
672
+ land_type: (
673
+ shares_of_expansion[land_type] * expansion_factor * e9_net_expansion * net_expansion_cultivated_vs_harvested
674
+ )
675
+ for land_type in LAND_USE_TERMS_FOR_TRANSFORMATION.keys()
676
+ if land_type != land_use_type
677
+ }
678
+ site_area[land_use_type] = 1 - sum(site_area.values())
679
+
680
+ sum_of_site_areas_is_100 = site_area_sum_to_100(site_area)
681
+ logRequirements(
682
+ log_node=site,
683
+ model=MODEL,
684
+ term=term.get("@id"),
685
+ model_key=MODEL_KEY,
686
+ land_use_type=land_use_type,
687
+ country_id=country_id,
688
+ site_area=log_as_table(site_area),
689
+ sum_of_site_areas_is_100=sum_of_site_areas_is_100
690
+ )
691
+
692
+ should_run = all(
693
+ [
694
+ site.get("siteType"),
695
+ country_id,
696
+ non_empty_value(term),
697
+ site.get("siteType") in SITE_TYPES,
698
+ sum_of_site_areas_is_100
699
+ ]
700
+ )
701
+ logShouldRun(site, MODEL, term=term.get("@id"), should_run=should_run, key=MODEL_KEY)
702
+
703
+ return should_run, site_area
704
+
705
+
706
+ def _should_run(site: dict) -> tuple[bool, dict]:
707
+ management_nodes = filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
708
+ land_use_type = _lookup_land_use_type(nodes=management_nodes)
709
+ should_run_result, site_area = (
710
+ (False, {}) if land_use_type not in {ANNUAL_CROPLAND, PERMANENT_CROPLAND, PERMANENT_PASTURE}
711
+ else _should_run_historical_land_use_change(
712
+ site=site,
713
+ land_use_type=land_use_type
714
+ )
715
+ )
716
+
717
+ return should_run_result, site_area
718
+
719
+
720
+ def run(site: dict) -> list:
721
+ should_run, site_area = _should_run(site)
722
+ management_nodes = filter_list_term_type(site.get("management", []), TermTermType.LANDCOVER)
723
+ return _run_make_management_nodes(
724
+ existing_nodes=management_nodes,
725
+ percentage_transformed_from=site_area,
726
+ start_year=int(management_nodes[0].get("endDate")[:4]) - DEFAULT_WINDOW_IN_YEARS
727
+ ) if should_run else []