hestia-earth-models 0.61.8__py3-none-any.whl → 0.62.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.

Potentially problematic release.


This version of hestia-earth-models might be problematic. Click here for more details.

Files changed (64) hide show
  1. hestia_earth/models/blonkConsultants2016/utils.py +3 -2
  2. hestia_earth/models/cycle/completeness/electricityFuel.py +5 -1
  3. hestia_earth/models/cycle/post_checks/__init__.py +3 -2
  4. hestia_earth/models/cycle/post_checks/otherSites.py +40 -0
  5. hestia_earth/models/cycle/pre_checks/__init__.py +2 -1
  6. hestia_earth/models/cycle/pre_checks/otherSites.py +42 -0
  7. hestia_earth/models/cycle/pre_checks/site.py +1 -1
  8. hestia_earth/models/cycle/product/economicValueShare.py +47 -31
  9. hestia_earth/models/ecoinventV3AndEmberClimate/utils.py +1 -1
  10. hestia_earth/models/emepEea2019/utils.py +4 -3
  11. hestia_earth/models/geospatialDatabase/heavyWinterPrecipitation.py +1 -1
  12. hestia_earth/models/ipcc2019/animal/pastureGrass.py +36 -30
  13. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +30 -4
  14. hestia_earth/models/ipcc2019/n2OToAirExcretaDirect.py +6 -2
  15. hestia_earth/models/ipcc2019/n2OToAirExcretaIndirect.py +1 -1
  16. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserDirect.py +1 -1
  17. hestia_earth/models/ipcc2019/n2OToAirInorganicFertiliserIndirect.py +1 -1
  18. hestia_earth/models/ipcc2019/n2OToAirOrganicFertiliserIndirect.py +1 -1
  19. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +4 -6
  20. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +4 -2
  21. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +12 -11
  22. hestia_earth/models/ipcc2019/pastureGrass.py +40 -22
  23. hestia_earth/models/ipcc2019/pastureGrass_utils.py +43 -60
  24. hestia_earth/models/mocking/search-results.json +261 -257
  25. hestia_earth/models/schererPfister2015/utils.py +2 -2
  26. hestia_earth/models/site/brackishWater.py +1 -1
  27. hestia_earth/models/site/flowingWater.py +1 -1
  28. hestia_earth/models/site/freshWater.py +1 -1
  29. hestia_earth/models/site/management.py +29 -11
  30. hestia_earth/models/site/pre_checks/cache_sources.py +9 -13
  31. hestia_earth/models/site/salineWater.py +1 -1
  32. hestia_earth/models/stehfestBouwman2006/n2OToAirCropResidueDecompositionDirect.py +12 -2
  33. hestia_earth/models/stehfestBouwman2006/n2OToAirExcretaDirect.py +12 -2
  34. hestia_earth/models/stehfestBouwman2006/n2OToAirInorganicFertiliserDirect.py +11 -1
  35. hestia_earth/models/stehfestBouwman2006/n2OToAirOrganicFertiliserDirect.py +11 -1
  36. hestia_earth/models/stehfestBouwman2006/noxToAirCropResidueDecomposition.py +12 -2
  37. hestia_earth/models/stehfestBouwman2006/noxToAirExcreta.py +12 -2
  38. hestia_earth/models/stehfestBouwman2006/noxToAirInorganicFertiliser.py +11 -1
  39. hestia_earth/models/stehfestBouwman2006/noxToAirOrganicFertiliser.py +11 -1
  40. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirCropResidueDecomposition.py +12 -2
  41. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirExcreta.py +12 -2
  42. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirInorganicFertiliser.py +11 -1
  43. hestia_earth/models/stehfestBouwman2006GisImplementation/noxToAirOrganicFertiliser.py +11 -1
  44. hestia_earth/models/utils/blank_node.py +159 -146
  45. hestia_earth/models/utils/constant.py +2 -0
  46. hestia_earth/models/utils/lookup.py +19 -6
  47. hestia_earth/models/utils/source.py +1 -1
  48. hestia_earth/models/version.py +1 -1
  49. {hestia_earth_models-0.61.8.dist-info → hestia_earth_models-0.62.1.dist-info}/METADATA +3 -3
  50. {hestia_earth_models-0.61.8.dist-info → hestia_earth_models-0.62.1.dist-info}/RECORD +64 -59
  51. tests/models/cycle/post_checks/test_otherSites.py +15 -0
  52. tests/models/cycle/pre_checks/test_otherSites.py +21 -0
  53. tests/models/cycle/product/test_economicValueShare.py +8 -0
  54. tests/models/ipcc2019/animal/test_pastureGrass.py +2 -2
  55. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +5 -2
  56. tests/models/ipcc2019/test_organicCarbonPerHa.py +2 -1
  57. tests/models/ipcc2019/test_pastureGrass.py +0 -16
  58. tests/models/site/pre_checks/test_cache_sources.py +6 -10
  59. tests/models/site/test_management.py +162 -2
  60. tests/models/utils/test_blank_node.py +154 -296
  61. tests/models/utils/test_lookup.py +10 -0
  62. {hestia_earth_models-0.61.8.dist-info → hestia_earth_models-0.62.1.dist-info}/LICENSE +0 -0
  63. {hestia_earth_models-0.61.8.dist-info → hestia_earth_models-0.62.1.dist-info}/WHEEL +0 -0
  64. {hestia_earth_models-0.61.8.dist-info → hestia_earth_models-0.62.1.dist-info}/top_level.txt +0 -0
@@ -25,5 +25,6 @@ def get_emission_factor(term_id: str, cycle: dict, factor: str):
25
25
  data = safe_parse_float(value, None)
26
26
  # fallback to site.siteType data if possible
27
27
  return data if data is not None else safe_parse_float(
28
- extract_grouped_data(
29
- get_table_value(lookup, 'termid', country_id, column_name('NONE')), site.get('siteType')), None)
28
+ extract_grouped_data(get_table_value(lookup, 'termid', country_id, column_name('NONE')), site.get('siteType')),
29
+ None
30
+ )
@@ -9,10 +9,12 @@ from hestia_earth.utils.model import filter_list_term_type
9
9
 
10
10
  from hestia_earth.models.log import logRequirements, log_as_table
11
11
  from hestia_earth.models.utils.blank_node import get_lookup_value
12
+ from hestia_earth.models.utils.completeness import _is_term_type_complete
12
13
  from . import MODEL
13
14
 
14
15
  REQUIREMENTS = {
15
16
  "Cycle": {
17
+ "completeness.operation": "True",
16
18
  "completeness.electricityFuel": "False",
17
19
  "practices": [
18
20
  {"@type": "Practice", "value": "", "term.termType": "operation"}
@@ -42,6 +44,7 @@ def _practice_value(practice: dict):
42
44
 
43
45
 
44
46
  def run(cycle: dict):
47
+ operation_complete = _is_term_type_complete(cycle, TermTermType.OPERATION)
45
48
  practices = filter_list_term_type(cycle.get('practices', []), TermTermType.OPERATION)
46
49
  combustion_practices = [
47
50
  p for p in practices if _lookup_value(p, LOOKUPS['operation'][1]) in _VALID_COMBUSTION_TYPES
@@ -49,8 +52,9 @@ def run(cycle: dict):
49
52
  practices_values = list(map(_practice_value, combustion_practices))
50
53
 
51
54
  logRequirements(cycle, model=MODEL, term=None, key=MODEL_KEY,
55
+ term_type_operation_complete=operation_complete,
52
56
  values=log_as_table(practices_values))
53
57
 
54
- is_complete = all([p.get('fuel_use') for p in practices_values])
58
+ is_complete = all([operation_complete] + [p.get('fuel_use') for p in practices_values])
55
59
 
56
60
  return is_complete
@@ -2,14 +2,15 @@ from os.path import dirname, abspath
2
2
  import sys
3
3
 
4
4
  from hestia_earth.models.utils import _run_in_serie
5
- from . import cache, site
5
+ from . import cache, site, otherSites
6
6
 
7
7
  CURRENT_DIR = dirname(abspath(__file__)) + '/'
8
8
  sys.path.append(CURRENT_DIR)
9
9
 
10
10
  MODELS = [
11
11
  cache.run,
12
- site.run
12
+ site.run,
13
+ otherSites.run
13
14
  ]
14
15
 
15
16
 
@@ -0,0 +1,40 @@
1
+ """
2
+ Post Checks Other Sites
3
+
4
+ This model is run only if the [pre model](../pre_checks/cycle.md) has been run before.
5
+ This model will restore the `cycle.otherSites` as a list of "linked node"
6
+ (i.e. it will be set with only `@type`, `@id` and `name` keys).
7
+ """
8
+ from hestia_earth.utils.model import linked_node
9
+
10
+ REQUIREMENTS = {
11
+ "Cycle": {
12
+ "otherSites": [{
13
+ "@type": "Site",
14
+ "@id": ""
15
+ }]
16
+ }
17
+ }
18
+ RETURNS = {
19
+ "Cycle": {
20
+ "otherSites": [{"@type": "Site"}]
21
+ }
22
+ }
23
+ MODEL_KEY = 'otherSites'
24
+
25
+
26
+ def _run_site(site: dict): return linked_node(site)
27
+
28
+
29
+ def _should_run_site(site: dict): return site.get('@id') is not None
30
+
31
+
32
+ def _should_run(cycle: dict): return len(cycle.get(MODEL_KEY, [])) > 0
33
+
34
+
35
+ def run(cycle: dict):
36
+ return cycle | (
37
+ ({
38
+ MODEL_KEY: [_run_site(site) if _should_run_site(site) else site for site in cycle.get(MODEL_KEY, [])]
39
+ }) if _should_run(cycle) else {}
40
+ )
@@ -2,12 +2,13 @@ from os.path import dirname, abspath
2
2
  import sys
3
3
 
4
4
  from hestia_earth.models.utils import _run_in_serie
5
- from . import site, cache_sources
5
+ from . import site, cache_sources, otherSites
6
6
 
7
7
  CURRENT_DIR = dirname(abspath(__file__)) + '/'
8
8
  sys.path.append(CURRENT_DIR)
9
9
 
10
10
  MODELS = [
11
+ otherSites.run,
11
12
  site.run,
12
13
  cache_sources.run
13
14
  ]
@@ -0,0 +1,42 @@
1
+ """
2
+ Pre Checks Other Sites
3
+
4
+ Some Cycle models need a full version of the linked [Site](https://hestia.earth/schema/Site) to run.
5
+ This model will fetch the complete version of the [Cycle Sites](https://hestia.earth/schema/Cycle#otherSites)
6
+ and include them.
7
+ """
8
+ from hestia_earth.schema import SchemaType
9
+
10
+ from hestia_earth.models.utils import _load_calculated_node
11
+
12
+ REQUIREMENTS = {
13
+ "Cycle": {
14
+ "otherSites": [{
15
+ "@type": "Site",
16
+ "@id": ""
17
+ }]
18
+ }
19
+ }
20
+ RETURNS = {
21
+ "Cycle": {
22
+ "otherSites": [{"@type": "Site"}]
23
+ }
24
+ }
25
+ MODEL_KEY = 'otherSites'
26
+
27
+
28
+ def _run_site(site: dict): return _load_calculated_node(site, SchemaType.SITE)
29
+
30
+
31
+ def _should_run_site(site: dict): return site.get('@id') is not None
32
+
33
+
34
+ def _should_run(cycle: dict): return len(cycle.get(MODEL_KEY, [])) > 0
35
+
36
+
37
+ def run(cycle: dict):
38
+ return cycle | (
39
+ ({
40
+ MODEL_KEY: [_run_site(site) if _should_run_site(site) else site for site in cycle.get(MODEL_KEY, [])]
41
+ }) if _should_run(cycle) else {}
42
+ )
@@ -2,7 +2,7 @@
2
2
  Pre Checks Site
3
3
 
4
4
  Some Cycle models need a full version of the linked [Site](https://hestia.earth/schema/Site) to run.
5
- This model will fetch the complete version of the [Site](https://hestia.earth/schema/Site) and include it.
5
+ This model will fetch the complete version of the [Cycle Site](https://hestia.earth/schema/Cycle#site) and include it.
6
6
  """
7
7
  from hestia_earth.schema import SchemaType
8
8
 
@@ -2,20 +2,21 @@
2
2
  Product Economic Value Share
3
3
 
4
4
  This model quantifies the relative economic value share of each marketable Product in a Cycle.
5
- Marketable Products are all Products in the Glossary with the exception of crop residue not sold.
5
+ Marketable Products are all Products identified with the lookup "generateImpactAssessment = TRUE".
6
6
 
7
7
  It works in the following order:
8
- 1. If revenue data are provided for all marketable products,
8
+ 1. For every product with no `value`, the `economicValueShare` is set to `0`.
9
+ 2. If revenue data are provided for all marketable products,
9
10
  the `economicValueShare` is directly calculated as the share of revenue of each Product;
10
- 2. If the primary product is a crop and it is the only crop Product,
11
- `economicValueShare` is assigned based on a lookup table containing typical global average economic value shares
12
- drawn from [Poore & Nemecek (2018)](https://science.sciencemag.org/content/360/6392/987).
11
+ 3. Based on a lookup table containing typical global average economic value shares
12
+ drawn from [Poore & Nemecek (2018)](https://science.sciencemag.org/content/360/6392/987), if all products have a
13
+ corresponding value, the `economicValueShare` will be proportionally distributed among the products.
13
14
  """
14
- from hestia_earth.schema import TermTermType
15
15
  from hestia_earth.utils.model import find_term_match
16
16
  from hestia_earth.utils.tools import list_sum
17
17
 
18
18
  from hestia_earth.models.log import logRequirements, logShouldRun
19
+ from hestia_earth.models.utils.blank_node import get_lookup_value
19
20
  from hestia_earth.models.utils.cycle import unique_currencies
20
21
  from .utils import lookup_share
21
22
  from .. import MODEL
@@ -25,9 +26,11 @@ REQUIREMENTS = {
25
26
  "products": [{
26
27
  "@type": "Product",
27
28
  "value": "",
28
- "economicValueShare": "",
29
- "revenue": "",
30
- "currency": ""
29
+ "optional": {
30
+ "economicValueShare": "",
31
+ "revenue": "",
32
+ "currency": ""
33
+ }
31
34
  }],
32
35
  "optional": {
33
36
  "completeness.product": ""
@@ -42,7 +45,8 @@ RETURNS = {
42
45
  LOOKUPS = {
43
46
  "@doc": "Depending on the primary product [termType](https://hestia.earth/schema/Product#term)",
44
47
  "crop": "global_economic_value_share",
45
- "excreta": "global_economic_value_share"
48
+ "excreta": "global_economic_value_share",
49
+ "animalProduct": "global_economic_value_share"
46
50
  }
47
51
  MODEL_KEY = 'economicValueShare'
48
52
  MAX_VALUE = 100.5
@@ -60,7 +64,7 @@ def _is_complete(cycle: dict): return cycle.get('completeness', {}).get('product
60
64
  def _no_yield(product): return list_sum(product.get('value', [-1]), -1) == 0
61
65
 
62
66
 
63
- def _total_revenue(products: list): return sum([p.get(MODEL_KEY, 0) for p in products])
67
+ def _total_evs(products: list): return sum([p.get(MODEL_KEY, 0) for p in products])
64
68
 
65
69
 
66
70
  def _product_with_value(product: dict):
@@ -72,18 +76,33 @@ def _rescale_value(products: list, total: float):
72
76
  return list(map(lambda p: {**p, MODEL_KEY: p.get(MODEL_KEY) * 100 / total}, products))
73
77
 
74
78
 
79
+ def _should_run_by_default(cycle: dict, products: list):
80
+ run_by = 'default'
81
+ # only run if all products have the lookup data or value will be incorrect
82
+ products = list(map(_product_with_value, products))
83
+ all_with_economicValueShare = all([p.get(MODEL_KEY) is not None for p in products])
84
+
85
+ should_run = all([all_with_economicValueShare])
86
+
87
+ for p in products:
88
+ term_id = p.get('term', {}).get('@id')
89
+ logRequirements(cycle, model=MODEL, term=term_id, key=MODEL_KEY, by=run_by,
90
+ all_with_economicValueShare=all_with_economicValueShare)
91
+ logShouldRun(cycle, MODEL, term_id, should_run, key=MODEL_KEY, by=run_by)
92
+
93
+ return should_run
94
+
95
+
75
96
  def _run_by_default(cycle: dict, products: list):
76
97
  run_by = 'default'
77
98
  is_complete = _is_complete(cycle)
78
99
  products = list(map(_product_with_value, products))
79
- # remove results where lookup share was not found
80
- results = list(filter(lambda p: p.get(MODEL_KEY) is not None, products))
81
100
  # only return list if the new total of evs is not above threshold
82
- total_revenue = _total_revenue(results)
83
- below_max_threshold = total_revenue <= MAX_VALUE
84
- should_rescale = is_complete and MIN_COMPLETE_VALUE <= total_revenue <= MAX_VALUE
85
- above_min_threshold = True if should_rescale else total_revenue >= MIN_VALUE if is_complete else True
86
- results = _rescale_value(results, total_revenue) if should_rescale else results
101
+ total_economicValueShare = _total_evs(products)
102
+ below_max_threshold = total_economicValueShare <= MAX_VALUE
103
+ should_rescale = is_complete and MIN_COMPLETE_VALUE <= total_economicValueShare <= MAX_VALUE
104
+ above_min_threshold = True if should_rescale else total_economicValueShare >= MIN_VALUE if is_complete else True
105
+ results = _rescale_value(products, total_economicValueShare) if should_rescale else products
87
106
 
88
107
  should_run = all([below_max_threshold, above_min_threshold])
89
108
 
@@ -93,7 +112,7 @@ def _run_by_default(cycle: dict, products: list):
93
112
  logRequirements(cycle, model=MODEL, term=term_id, key=MODEL_KEY, by=run_by,
94
113
  below_max_threshold=below_max_threshold,
95
114
  above_min_threshold=above_min_threshold,
96
- total_revenue=total_revenue)
115
+ total_economicValueShare=total_economicValueShare)
97
116
  logShouldRun(cycle, MODEL, term_id, p_should_run, key=MODEL_KEY, by=run_by)
98
117
 
99
118
  return results if should_run else []
@@ -109,8 +128,8 @@ def _run_by_revenue(products: list):
109
128
  def _should_run_by_revenue(cycle: dict, products: list):
110
129
  run_by = 'revenue'
111
130
  is_complete = _is_complete(cycle)
112
- total_value = _total_revenue(products)
113
- below_threshold = total_value < MAX_VALUE
131
+ total_economicValueShare = _total_evs(products)
132
+ below_threshold = total_economicValueShare < MAX_VALUE
114
133
  # ignore products with no yield
115
134
  products = list(filter(lambda p: not _no_yield(p), products))
116
135
  currencies = unique_currencies(cycle)
@@ -122,7 +141,7 @@ def _should_run_by_revenue(cycle: dict, products: list):
122
141
  term_id = p.get('term', {}).get('@id')
123
142
  logRequirements(cycle, model=MODEL, term=term_id, key=MODEL_KEY, by=run_by,
124
143
  is_complete=is_complete,
125
- total_value=total_value,
144
+ total_economicValueShare=total_economicValueShare,
126
145
  below_threshold=below_threshold,
127
146
  all_with_revenue=all_with_revenue,
128
147
  currencies=';'.join(currencies),
@@ -133,14 +152,14 @@ def _should_run_by_revenue(cycle: dict, products: list):
133
152
 
134
153
 
135
154
  def _run_single_missing_evs(products: list):
136
- total_value = _total_revenue(products)
155
+ total_value = _total_evs(products)
137
156
  return list(map(lambda p: _product(p, 100 - total_value) if p.get(MODEL_KEY) is None else p, products))
138
157
 
139
158
 
140
159
  def _should_run_single_missing_evs(cycle: dict, products: list):
141
160
  run_by = '1-missing-evs'
142
161
  is_complete = _is_complete(cycle)
143
- total_value = _total_revenue(products)
162
+ total_value = _total_evs(products)
144
163
  # ignore products with no yield
145
164
  products = list(filter(lambda p: not _no_yield(p), products))
146
165
  missing_values = [p for p in products if p.get(MODEL_KEY) is None]
@@ -175,20 +194,17 @@ def _should_run_no_value(cycle: dict, product: dict):
175
194
 
176
195
 
177
196
  def _should_have_evs(product: dict):
178
- term_type = product.get('term', {}).get('termType')
179
- return term_type not in [
180
- TermTermType.CROPRESIDUE.value,
181
- TermTermType.EXCRETA.value
182
- ]
197
+ should_generate_ia = get_lookup_value(product.get('term', {}), 'generateImpactAssessment')
198
+ return str(should_generate_ia).lower() != 'false'
183
199
 
184
200
 
185
201
  def run(cycle: dict):
186
202
  products = cycle.get('products', [])
187
- # skip any product that will never has value
203
+ # skip any product that is not marketable
188
204
  products = list(filter(_should_have_evs, products))
189
205
  products = list(map(lambda p: _product(p, 0) if _should_run_no_value(cycle, p) else p, products))
190
206
  return (
191
207
  _run_single_missing_evs(products) if _should_run_single_missing_evs(cycle, products) else
192
208
  _run_by_revenue(products) if _should_run_by_revenue(cycle, products) else
193
- _run_by_default(cycle, products)
209
+ _run_by_default(cycle, products) if _should_run_by_default(cycle, products) else []
194
210
  )
@@ -33,7 +33,7 @@ def _extract_emission_value(value_iter: Any) -> Union[float, None]:
33
33
  value_list = list(value_iter)
34
34
  try:
35
35
  if len(list(value_list)) > 0 and len(list(value_list)[0]) > 1:
36
- return safe_parse_float(list(value_list)[0][1])
36
+ return safe_parse_float(list(value_list)[0][1], None)
37
37
  except ValueError:
38
38
  return None
39
39
 
@@ -1,7 +1,7 @@
1
1
  from hestia_earth.schema import TermTermType
2
2
  from hestia_earth.utils.model import filter_list_term_type
3
3
  from hestia_earth.utils.lookup import extract_grouped_data
4
- 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
5
5
 
6
6
  from hestia_earth.models.utils.completeness import _is_term_type_complete
7
7
  from hestia_earth.models.utils.term import get_lookup_value
@@ -19,14 +19,15 @@ def _get_fuel_input_value(term_id: str, lookup_col: str):
19
19
  get_lookup_value(operation_term, lookup_col, model=MODEL, term=term_id), input_term_id
20
20
  ) if operation_term else None
21
21
  input_factor = operation_factor or get_lookup_value(input_term, lookup_col, model=MODEL, term=term_id)
22
+ factor = safe_parse_float(input_factor, None)
22
23
 
23
- return input_value * safe_parse_float(input_factor)
24
+ return input_value * factor if factor is not None else None
24
25
  return get_value
25
26
 
26
27
 
27
28
  def get_fuel_values(term_id: str, cycle: dict, lookup_col: str):
28
29
  inputs = filter_list_term_type(cycle.get('inputs', []), TermTermType.FUEL)
29
- values = list(map(_get_fuel_input_value(term_id, lookup_col), inputs))
30
+ values = non_empty_list(map(_get_fuel_input_value(term_id, lookup_col), inputs))
30
31
 
31
32
  return [0] if all([
32
33
  len(values) == 0,
@@ -39,7 +39,7 @@ def _measurement(site: dict, value: float):
39
39
 
40
40
  def _download(site: dict):
41
41
  value = download(TERM_ID, site, EE_PARAMS)
42
- return 1 if value == 1 else (0 if value == 0.1 else None)
42
+ return value == 1
43
43
 
44
44
 
45
45
  def _run(site: dict):
@@ -1,9 +1,12 @@
1
1
  """
2
- Full Grass Consumption
2
+ Animal Pasture Grass
3
3
 
4
4
  This model estimates the energetic requirements of ruminants and can be used to estimate the amount of grass they graze.
5
5
  Source:
6
6
  [IPCC 2019, Vol.4, Chapter 10](https://www.ipcc-nggip.iges.or.jp/public/2019rf/pdf/4_Volume4/19R_V4_Ch10_Livestock.pdf).
7
+
8
+ This version of the model will run at the Animal Blank Node level, if none of the Cycle Input are given as feed
9
+ (see https://www.hestia.earth/schema/Input#isAnimalFeed).
7
10
  """
8
11
  from hestia_earth.schema import TermTermType
9
12
  from hestia_earth.utils.model import filter_list_term_type
@@ -138,7 +141,7 @@ RETURNS = {
138
141
  }]
139
142
  }]
140
143
  }
141
- MODEL_KEY = 'animal/pastureGrass'
144
+ MODEL_KEY = 'pastureGrass'
142
145
 
143
146
 
144
147
  def _input(term_id: str, value: float):
@@ -173,7 +176,7 @@ def calculate_NEwool(cycle: dict, animal: dict, products: list, total_weight: fl
173
176
  ])
174
177
  animal_weight = _sum_liveWeightPerHead([animal])
175
178
 
176
- debugValues(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
179
+ debugValues(animal, model=MODEL, term=term_id, model_key=MODEL_KEY,
177
180
  total_energy=total_energy,
178
181
  animal_liveWeightPerHead=animal_weight,
179
182
  total_liveWeightPerHead=total_weight)
@@ -186,29 +189,35 @@ def _calculate_GE(
186
189
  ) -> float:
187
190
  term_id = animal.get('term', {}).get('@id')
188
191
 
189
- NEm, NEa, NEl, NEwork, NEp, NEg = get_animal_values(cycle, animal, system)
192
+ NEm, NEa, NEl, NEwork, NEp, NEg = get_animal_values(cycle, animal, system, log_node=animal)
190
193
 
191
194
  NEm_feed, NEg_feed = calculate_NEfeed(animal)
192
- debugValues(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
195
+ debugValues(animal, model=MODEL, term=term_id, model_key=MODEL_KEY,
196
+ NEm=NEm,
197
+ NEa=NEa,
198
+ NEl=NEl,
199
+ NEwork=NEwork,
200
+ NEp=NEp,
201
+ NEg=NEg,
193
202
  NEm_feed=NEm_feed,
194
203
  NEg_feed=NEg_feed)
195
204
 
196
205
  return (NEm + NEa + NEl + NEwork + NEp - NEm_feed)/REM + (NEg + NEwool - NEg_feed)/REG
197
206
 
198
207
 
199
- def _run_practice(cycle: dict, GE: float, meanECHHV: float):
208
+ def _run_practice(animal: dict, GE: float, meanECHHV: float):
200
209
  def run(practice: dict):
201
210
  key = practice.get('key', {})
202
211
  key_id = key.get('@id')
203
212
  input_term_id = practice_input_id(practice)
204
213
  value = (GE / meanECHHV) * (list_sum(practice.get('value', [0])) / 100)
205
214
 
206
- logRequirements(cycle, model=MODEL, term=input_term_id, model_key=MODEL_KEY,
215
+ logRequirements(animal, model=MODEL, term=input_term_id, model_key=MODEL_KEY,
207
216
  GE=GE,
208
217
  meanECHHV=meanECHHV,
209
- key_id=key_id)
218
+ practice_key_id=key_id)
210
219
 
211
- logShouldRun(cycle, MODEL, input_term_id, True, model_key=MODEL_KEY)
220
+ logShouldRun(animal, MODEL, input_term_id, True, model_key=MODEL_KEY)
212
221
 
213
222
  return _input(input_term_id, value)
214
223
 
@@ -233,14 +242,14 @@ def _run_animal(cycle: dict, meanDE: float, meanECHHV: float, system: dict, prac
233
242
  ) else 0
234
243
  GE = (_calculate_GE(cycle, animal, REM, REG, NEwool, system) / (meanDE/100)) if all([REM, REG]) else 0
235
244
 
236
- debugValues(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
245
+ debugValues(animal, model=MODEL, term=term_id, model_key=MODEL_KEY,
237
246
  REM=REM,
238
247
  REG=REG,
239
248
  NEwool=NEwool,
240
249
  GE=GE,
241
250
  meanDE=meanDE)
242
251
 
243
- inputs = list(map(_run_practice(cycle, GE, meanECHHV), practices))
252
+ inputs = list(map(_run_practice(animal, GE, meanECHHV), practices))
244
253
  return animal | {
245
254
  'inputs': animal.get('inputs', []) + inputs
246
255
  }
@@ -248,17 +257,12 @@ def _run_animal(cycle: dict, meanDE: float, meanECHHV: float, system: dict, prac
248
257
  return run
249
258
 
250
259
 
251
- def _run(cycle: dict, meanDE: float, meanECHHV: float, system: dict, practices: list):
252
- animals = get_animals(cycle)
253
- return list(map(_run_animal(cycle, meanDE, meanECHHV, system, practices), animals))
254
-
255
-
256
- def _should_run(cycle: dict, practices: dict):
260
+ def _should_run(cycle: dict, animals: list, practices: dict):
257
261
  systems = filter_list_term_type(cycle.get('practices', []), TermTermType.SYSTEM)
258
262
  animalFeed_complete = _is_term_type_complete(cycle, 'animalFeed')
259
263
  animalPopulation_complete = _is_term_type_complete(cycle, 'animalPopulation')
260
264
  freshForage_incomplete = _is_term_type_incomplete(cycle, 'freshForage')
261
- all_animals_have_value = all([a.get('value', 0) > 0 for a in cycle.get('animals', [])])
265
+ all_animals_have_value = all([a.get('value', 0) > 0 for a in animals])
262
266
 
263
267
  no_cycle_inputs_feed = all([not input.get('isAnimalFeed', False) for input in cycle.get('inputs', [])])
264
268
 
@@ -278,21 +282,23 @@ def _should_run(cycle: dict, practices: dict):
278
282
  ])
279
283
 
280
284
  for term_id in [MODEL_KEY] + [practice_input_id(p) for p in practices]:
281
- logRequirements(cycle, model=MODEL, term=term_id, model_key=MODEL_KEY,
282
- term_type_animalFeed_complete=animalFeed_complete,
283
- term_type_animalPopulation_complete=animalPopulation_complete,
284
- term_type_freshForage_incomplete=freshForage_incomplete,
285
- no_cycle_inputs_feed=no_cycle_inputs_feed,
286
- all_animals_have_value=all_animals_have_value,
287
- meanDE=meanDE,
288
- meanECHHV=meanECHHV)
289
-
290
- logShouldRun(cycle, MODEL, term_id, should_run, model_key=MODEL_KEY)
285
+ for animal in animals:
286
+ logRequirements(animal, model=MODEL, term=term_id, model_key=MODEL_KEY,
287
+ term_type_animalFeed_complete=animalFeed_complete,
288
+ term_type_animalPopulation_complete=animalPopulation_complete,
289
+ term_type_freshForage_incomplete=freshForage_incomplete,
290
+ no_cycle_inputs_feed=no_cycle_inputs_feed,
291
+ all_animals_have_value=all_animals_have_value,
292
+ meanDE=meanDE,
293
+ meanECHHV=meanECHHV)
294
+
295
+ logShouldRun(animal, MODEL, term_id, should_run, model_key=MODEL_KEY)
291
296
 
292
297
  return should_run, meanDE, meanECHHV, systems[0] if systems else None
293
298
 
294
299
 
295
300
  def run(cycle: dict):
301
+ animals = get_animals(cycle)
296
302
  practices = list(filter(should_run_practice(cycle), cycle.get('practices', [])))
297
- should_run, meanDE, meanECHHV, system = _should_run(cycle, practices)
298
- return _run(cycle, meanDE, meanECHHV, system, practices) if should_run else []
303
+ should_run, meanDE, meanECHHV, system = _should_run(cycle, animals, practices)
304
+ return list(map(_run_animal(cycle, meanDE, meanECHHV, system, practices), animals)) if should_run else []
@@ -7,7 +7,7 @@ from pydash.objects import merge
7
7
  from typing import NamedTuple, Optional, Union
8
8
 
9
9
  from hestia_earth.schema import (
10
- CycleFunctionalUnit, EmissionMethodTier, MeasurementMethodClassification
10
+ CycleFunctionalUnit, EmissionMethodTier, MeasurementMethodClassification, SiteSiteType
11
11
  )
12
12
  from hestia_earth.utils.date import diff_in_days
13
13
  from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
@@ -15,7 +15,8 @@ from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_date
15
15
  from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
16
16
  from hestia_earth.models.utils import pairwise
17
17
  from hestia_earth.models.utils.blank_node import (
18
- _get_datestr_format, _gapfill_datestr, DatestrGapfillMode, DatestrFormat, group_nodes_by_year, node_term_match
18
+ _get_datestr_format, _gapfill_datestr, DatestrGapfillMode, DatestrFormat, group_nodes_by_year, node_term_match,
19
+ cumulative_nodes_term_match
19
20
  )
20
21
  from hestia_earth.models.utils.constant import Units, get_atomic_conversion
21
22
  from hestia_earth.models.utils.emission import _new_emission, min_emission_method_tier
@@ -82,6 +83,13 @@ The list of `MeasurementMethodClassification`s that can be used to calculate SOC
82
83
  order from strongest to weakest.
83
84
  """
84
85
 
86
+ _SITE_TYPE_SYSTEMS_MAPPING = {
87
+ SiteSiteType.GLASS_OR_HIGH_ACCESSIBLE_COVER.value: [
88
+ "protectedCroppingSystemSoilBased",
89
+ "protectedCroppingSystemSoilAndSubstrateBased"
90
+ ]
91
+ }
92
+
85
93
 
86
94
  class _InventoryKey(Enum):
87
95
  """
@@ -207,19 +215,37 @@ def _should_run(cycle: dict) -> tuple[bool, str, dict]:
207
215
  soc_measurements = [node for node in site.get("measurements", []) if _validate_soc_measurement(node)]
208
216
  cycles = related_cycles(site)
209
217
 
218
+ site_type = site.get("siteType")
219
+ has_soil = site_type not in _SITE_TYPE_SYSTEMS_MAPPING or all(
220
+ cumulative_nodes_term_match(
221
+ cycle.get("practices", []),
222
+ target_term_ids=_SITE_TYPE_SYSTEMS_MAPPING[site_type],
223
+ cumulative_threshold=0
224
+ ) for cycle in cycles
225
+ )
226
+
210
227
  has_soc_measurements = len(soc_measurements) > 0
211
228
  has_cycles = len(cycles) > 0
212
229
  has_functional_unit_1_ha = all(cycle.get('functionalUnit') == CycleFunctionalUnit._1_HA.value for cycle in cycles)
213
230
 
214
- should_compile_inventory = all([has_cycles, has_functional_unit_1_ha, has_soc_measurements])
231
+ should_compile_inventory = all([
232
+ has_soil,
233
+ has_cycles,
234
+ has_functional_unit_1_ha,
235
+ has_soc_measurements,
236
+ ])
215
237
 
216
- inventory, logs = _compile_inventory(cycle_id, cycles, soc_measurements) if should_compile_inventory else ({}, {})
238
+ inventory, logs = (
239
+ _compile_inventory(cycle_id, cycles, soc_measurements) if should_compile_inventory else ({}, {})
240
+ )
217
241
 
218
242
  has_valid_inventory = len(inventory) > 0
219
243
  has_consecutive_years = check_consecutive(inventory.keys())
220
244
 
221
245
  logRequirements(
222
246
  cycle, model=MODEL, term=TERM_ID,
247
+ site_type=site_type,
248
+ has_soil=has_soil,
223
249
  has_soc_measurements=has_soc_measurements,
224
250
  has_cycles=has_cycles,
225
251
  has_functional_unit_1_ha=has_functional_unit_1_ha,
@@ -1,7 +1,8 @@
1
- from hestia_earth.schema import EmissionMethodTier
1
+ from hestia_earth.schema import EmissionMethodTier, TermTermType
2
2
 
3
3
  from hestia_earth.models.log import logRequirements, logShouldRun
4
4
  from hestia_earth.models.utils.constant import Units, get_atomic_conversion
5
+ from hestia_earth.models.utils.completeness import _is_term_type_complete
5
6
  from hestia_earth.models.utils.emission import _new_emission
6
7
  from hestia_earth.models.utils.input import total_excreta
7
8
  from hestia_earth.models.utils.excretaManagement import get_lookup_factor
@@ -9,6 +10,7 @@ from . import MODEL
9
10
 
10
11
  REQUIREMENTS = {
11
12
  "Cycle": {
13
+ "completeness.excreta": "True",
12
14
  "practices": [
13
15
  {"@type": "Practice", "value": "", "term.termType": "excretaManagement"}
14
16
  ]
@@ -42,10 +44,12 @@ def _run(excretaKgN: float, N2O_N_EF: float):
42
44
  def _should_run(cycle: dict):
43
45
  excretaKgN = total_excreta(cycle.get('inputs', []))
44
46
  N2O_N_EF = get_lookup_factor(cycle.get('practices', []), LOOKUPS['excretaManagement'])
47
+ term_type_complete = _is_term_type_complete(cycle, TermTermType.EXCRETA)
45
48
 
46
49
  logRequirements(cycle, model=MODEL, term=TERM_ID,
47
50
  excretaKgN=excretaKgN,
48
- N2O_N_EF=N2O_N_EF)
51
+ N2O_N_EF=N2O_N_EF,
52
+ term_type_excreta_complete=term_type_complete)
49
53
 
50
54
  should_run = all([excretaKgN, N2O_N_EF])
51
55
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
@@ -88,7 +88,7 @@ def _should_run(cycle: dict):
88
88
  no3_n=no3_n,
89
89
  nh3_n=nh3_n,
90
90
  nox_n=nox_n,
91
- term_type_cropResidue_complete=term_type_complete)
91
+ term_type_excreta_complete=term_type_complete)
92
92
 
93
93
  should_run = all([no3_n is not None, nh3_n is not None, nox_n is not None, term_type_complete])
94
94
  logShouldRun(cycle, MODEL, TERM_ID, should_run, methodTier=TIER)
@@ -97,7 +97,7 @@ def _should_run(cycle: dict):
97
97
  flooded_rice = has_flooded_rice(cycle.get('products', []))
98
98
 
99
99
  logRequirements(cycle, model=MODEL, term=TERM_ID,
100
- term_type_cropResidue_complete=term_type_complete,
100
+ term_type_fertiliser_complete=term_type_complete,
101
101
  N_total=N_total,
102
102
  has_flooded_rice=flooded_rice,
103
103
  ecoClimateZone=ecoClimateZone)