hestia-earth-models 0.61.6__py3-none-any.whl → 0.61.8__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 (52) hide show
  1. hestia_earth/models/cycle/completeness/electricityFuel.py +56 -0
  2. hestia_earth/models/cycle/input/hestiaAggregatedData.py +1 -1
  3. hestia_earth/models/emepEea2019/nh3ToAirInorganicFertiliser.py +44 -59
  4. hestia_earth/models/geospatialDatabase/histosol.py +4 -0
  5. hestia_earth/models/ipcc2006/co2ToAirOrganicSoilCultivation.py +4 -2
  6. hestia_earth/models/ipcc2006/n2OToAirOrganicSoilCultivationDirect.py +1 -1
  7. hestia_earth/models/ipcc2019/aboveGroundCropResidueTotal.py +1 -1
  8. hestia_earth/models/ipcc2019/belowGroundCropResidue.py +1 -1
  9. hestia_earth/models/ipcc2019/ch4ToAirExcreta.py +1 -1
  10. hestia_earth/models/ipcc2019/co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +511 -458
  11. hestia_earth/models/ipcc2019/co2ToAirUreaHydrolysis.py +5 -1
  12. hestia_earth/models/ipcc2019/organicCarbonPerHa.py +117 -3881
  13. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_1_utils.py +2060 -0
  14. hestia_earth/models/ipcc2019/organicCarbonPerHa_tier_2_utils.py +1630 -0
  15. hestia_earth/models/ipcc2019/organicCarbonPerHa_utils.py +324 -0
  16. hestia_earth/models/mocking/search-results.json +360 -260
  17. hestia_earth/models/schererPfister2015/pToDrainageWaterSoilFlux.py +1 -1
  18. hestia_earth/models/schererPfister2015/pToGroundwaterSoilFlux.py +1 -1
  19. hestia_earth/models/site/organicCarbonPerHa.py +58 -44
  20. hestia_earth/models/site/soilMeasurement.py +25 -38
  21. hestia_earth/models/utils/__init__.py +28 -0
  22. hestia_earth/models/utils/aquacultureManagement.py +2 -2
  23. hestia_earth/models/utils/array_builders.py +578 -0
  24. hestia_earth/models/utils/blank_node.py +2 -3
  25. hestia_earth/models/utils/crop.py +24 -1
  26. hestia_earth/models/utils/cycle.py +0 -23
  27. hestia_earth/models/utils/descriptive_stats.py +285 -0
  28. hestia_earth/models/utils/emission.py +73 -2
  29. hestia_earth/models/utils/inorganicFertiliser.py +2 -2
  30. hestia_earth/models/utils/lookup.py +6 -3
  31. hestia_earth/models/utils/measurement.py +118 -4
  32. hestia_earth/models/utils/site.py +25 -13
  33. hestia_earth/models/version.py +1 -1
  34. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/METADATA +1 -1
  35. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/RECORD +52 -40
  36. tests/models/cycle/completeness/test_electricityFuel.py +21 -0
  37. tests/models/emepEea2019/test_nh3ToAirInorganicFertiliser.py +2 -2
  38. tests/models/ipcc2019/test_co2ToAirSoilOrganicCarbonStockChangeManagementChange.py +54 -165
  39. tests/models/ipcc2019/test_organicCarbonPerHa.py +219 -460
  40. tests/models/ipcc2019/test_organicCarbonPerHa_tier_1_utils.py +471 -0
  41. tests/models/ipcc2019/test_organicCarbonPerHa_tier_2_utils.py +208 -0
  42. tests/models/ipcc2019/test_organicCarbonPerHa_utils.py +75 -0
  43. tests/models/site/test_organicCarbonPerHa.py +3 -12
  44. tests/models/site/test_soilMeasurement.py +5 -19
  45. tests/models/utils/test_array_builders.py +253 -0
  46. tests/models/utils/{test_cycle.py → test_crop.py} +2 -2
  47. tests/models/utils/test_descriptive_stats.py +134 -0
  48. tests/models/utils/test_emission.py +51 -1
  49. tests/models/utils/test_measurement.py +54 -2
  50. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/LICENSE +0 -0
  51. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/WHEEL +0 -0
  52. {hestia_earth_models-0.61.6.dist-info → hestia_earth_models-0.61.8.dist-info}/top_level.txt +0 -0
@@ -53,7 +53,7 @@ def _emission(value: float):
53
53
 
54
54
  def _run(cycle: dict, drainageClass: list):
55
55
  P_total, _ = get_liquid_slurry_sludge_P_total(cycle)
56
- value = 0.07 * (1 + P_total * 0.2/80) * (6 if drainageClass > 3 else 0)
56
+ value = 0.07 * (1 + P_total * 0.2/80) * (6 if drainageClass > 4 else 0)
57
57
  return [_emission(value)]
58
58
 
59
59
 
@@ -41,7 +41,7 @@ def _emission(value: float):
41
41
 
42
42
  def _run(cycle: dict, drainageClass: list):
43
43
  P_total, _ = get_liquid_slurry_sludge_P_total(cycle)
44
- value = 0.07 * (1 + P_total * 0.2/80) * (0 if drainageClass > 3 else 1)
44
+ value = 0.07 * (1 + P_total * 0.2/80) * (0 if drainageClass > 4 else 1)
45
45
  return [_emission(value)]
46
46
 
47
47
 
@@ -3,10 +3,10 @@ from typing import Optional, Union
3
3
  from hestia_earth.schema import MeasurementMethodClassification
4
4
  from hestia_earth.utils.date import diff_in_days
5
5
  from hestia_earth.utils.model import find_term_match
6
- from hestia_earth.utils.tools import flatten, safe_parse_float
6
+ from hestia_earth.utils.tools import flatten, non_empty_list, safe_parse_float
7
7
 
8
8
  from hestia_earth.models.log import log_as_table, logRequirements, logShouldRun
9
- from hestia_earth.models.utils.blank_node import _get_last_date, group_nodes_by_last_date
9
+ from hestia_earth.models.utils.blank_node import _get_last_date, group_nodes_by_last_date, node_term_match
10
10
  from hestia_earth.models.utils.measurement import (
11
11
  _new_measurement, group_measurements_by_depth, measurement_value, OLDEST_DATE
12
12
  )
@@ -21,14 +21,17 @@ REQUIREMENTS = {
21
21
  "value": "",
22
22
  "term.@id": "soilBulkDensity",
23
23
  "depthUpper": "",
24
- "depthLower": ""
24
+ "depthLower": "",
25
+ "methodClassification": ["on-site physical measurement", "modelled using other measurements"]
25
26
  },
26
27
  {
27
28
  "@type": "Measurement",
28
29
  "value": "",
30
+ "dates": "",
29
31
  "term.@id": "organicCarbonPerKgSoil",
30
32
  "depthUpper": "",
31
- "depthLower": ""
33
+ "depthLower": "",
34
+ "methodClassification": ["on-site physical measurement", "modelled using other measurements"]
32
35
  }
33
36
  ]
34
37
  }
@@ -36,6 +39,7 @@ REQUIREMENTS = {
36
39
  RETURNS = {
37
40
  "Measurement": [{
38
41
  "value": "",
42
+ "dates": "",
39
43
  "depthUpper": "",
40
44
  "depthLower": "",
41
45
  "methodClassification": "modelled using other measurements"
@@ -51,6 +55,10 @@ RESCALE_DEPTH_LOWER = 30
51
55
  MAX_DEPTH_LOWER = 100
52
56
  SOIL_BULK_DENSITY_TERM_ID = 'soilBulkDensity'
53
57
  ORGANIC_CARBON_PER_KG_SOIL_TERM_ID = 'organicCarbonPerKgSoil'
58
+ VALID_MEASUREMENT_METHOD_CLASSIFICATIONS = {
59
+ MeasurementMethodClassification.ON_SITE_PHYSICAL_MEASUREMENT.value,
60
+ MeasurementMethodClassification.MODELLED_USING_OTHER_MEASUREMENTS.value
61
+ }
54
62
 
55
63
 
56
64
  def _measurement(
@@ -98,38 +106,40 @@ def _calc_organic_carbon_per_ha(
98
106
  return (depth_lower - depth_upper) * soil_bulk_density * organic_carbon_per_kg_soil * 100
99
107
 
100
108
 
101
- def _should_run_calculation_group(nodes: list) -> bool:
109
+ def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
102
110
  """
103
- Determines whether a depth interval group has sufficient data to calculate `organicCarbonPerHa` from
104
- `soilBulkDensity` and `organicCarbonPerKgSoil`.
111
+ Pre-process site data and determine whether there is sufficient data to calculate `organicCarbonPerHa`.
105
112
  """
106
- soilBulkDensity = find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, None)
107
- has_soil_bulk_density_depth_lower = (soilBulkDensity or {}).get('depthLower') is not None
108
- has_soil_bulk_density_depth_upper = (soilBulkDensity or {}).get('depthUpper') is not None
113
+ oc_nodes = [node for node in site.get("measurements", []) if _valid_measurement(node, TERM_ID)]
109
114
 
110
- organicCarbonPerKgSoil = find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, None)
111
- has_organic_carbon_per_kg_soil_depth_lower = (organicCarbonPerKgSoil or {}).get('depthLower') is not None
112
- has_organic_carbon_per_kg_soil_depth_upper = (organicCarbonPerKgSoil or {}).get('depthUpper') is not None
115
+ # We don't need to run the model for any dates we already have an `organicCarbonPerHa` value for.
116
+ oc_node_dates = set(non_empty_list(flatten(measurement.get("dates", []) for measurement in oc_nodes)))
113
117
 
114
- return all([
115
- has_soil_bulk_density_depth_lower,
116
- has_soil_bulk_density_depth_upper,
117
- has_organic_carbon_per_kg_soil_depth_lower,
118
- has_organic_carbon_per_kg_soil_depth_upper
119
- ])
118
+ occ_nodes = [
119
+ node for node in site.get("measurements", [])
120
+ if all([
121
+ _valid_measurement(node, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID),
122
+ len(node.get("dates", [])) > 0,
123
+ _get_last_date(node.get("dates", [])) not in oc_node_dates
124
+ ])
125
+ ]
120
126
 
127
+ bd_nodes = [node for node in site.get("measurements", []) if _valid_measurement(node, SOIL_BULK_DENSITY_TERM_ID)]
121
128
 
122
- def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
123
- """
124
- Pre-process site data and determine whether there is sufficient data to calculate `organicCarbonPerHa`.
125
- """
126
- grouped_measurements = {
127
- depth_key: nodes for depth_key, nodes in group_measurements_by_depth(site.get('measurements', [])).items()
129
+ measurements = occ_nodes + bd_nodes
130
+ grouped_measurements = group_measurements_by_depth(measurements, include_dates=False)
131
+
132
+ inventory = {
133
+ depth_key: {
134
+ "measurements": nodes,
135
+ "has-soil-bulk-density": bool(find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID)),
136
+ "has-organic-carbon-per-kg-soil": bool(find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID))
137
+ } for depth_key, nodes in grouped_measurements.items()
128
138
  }
129
139
 
130
140
  valid_grouped_measurements = {
131
- depth_key: nodes for depth_key, nodes in grouped_measurements.items()
132
- if _should_run_calculation_group(nodes)
141
+ depth_key: group["measurements"] for depth_key, group in inventory.items()
142
+ if all([group["has-soil-bulk-density"], group["has-organic-carbon-per-kg-soil"]])
133
143
  }
134
144
 
135
145
  should_run = bool(valid_grouped_measurements)
@@ -139,20 +149,24 @@ def _should_run_calculation(site: dict) -> tuple[bool, dict[str, list[dict]]]:
139
149
  "inventory_calculation": log_as_table(
140
150
  {
141
151
  "depth-key": str(depth_key).replace("_", "-"),
142
- "should-run": depth_key in valid_grouped_measurements.keys(),
143
- "has-soil-bulk-density": (
144
- find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, {}).get('depthLower')
145
- and find_term_match(nodes, SOIL_BULK_DENSITY_TERM_ID, {}).get('depthUpper')
146
- ),
147
- "has-organic-carbon-per-kg-soil": (
148
- find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, {}).get('depthLower')
149
- and find_term_match(nodes, ORGANIC_CARBON_PER_KG_SOIL_TERM_ID, {}).get('depthUpper')
150
- )
151
- } for depth_key, nodes in grouped_measurements.items()
152
- ) or None
152
+ "should-run": depth_key in valid_grouped_measurements,
153
+ "has-soil-bulk-density": group["has-soil-bulk-density"],
154
+ "has-organic-carbon-per-kg-soil": group["has-organic-carbon-per-kg-soil"]
155
+ } for depth_key, group in inventory.items()
156
+ ) if inventory else "None"
153
157
  }
154
158
 
155
- return should_run, logs, valid_grouped_measurements
159
+ return should_run, valid_grouped_measurements, logs
160
+
161
+
162
+ def _valid_measurement(node: dict, target_term_id: str) -> bool:
163
+ return all([
164
+ node_term_match(node, target_term_id),
165
+ node.get("value"),
166
+ node.get("depthLower") is not None,
167
+ node.get("depthUpper") is not None,
168
+ node.get("methodClassification") in VALID_MEASUREMENT_METHOD_CLASSIFICATIONS
169
+ ])
156
170
 
157
171
 
158
172
  def _run_calculation(site: dict, depth_key: str, measurement_nodes: list[dict]) -> list[dict]:
@@ -332,11 +346,11 @@ def _should_run_rescale(organic_carbon_per_ha_nodes: list) -> tuple[bool, dict[s
332
346
  {
333
347
  "date": str(datestr),
334
348
  "should-run": datestr in valid_grouped_nodes.keys()
335
- } for datestr, nodes in grouped_nodes.items()
336
- ) or None
349
+ } for datestr in grouped_nodes.keys()
350
+ ) if grouped_nodes else "None"
337
351
  }
338
352
 
339
- return should_run, logs, valid_grouped_nodes
353
+ return should_run, valid_grouped_nodes, logs
340
354
 
341
355
 
342
356
  def _depth_distance(node: dict):
@@ -377,7 +391,7 @@ def _run_rescale(site: dict, organic_carbon_per_ha_nodes: list[dict]) -> list[di
377
391
 
378
392
 
379
393
  def run(site: dict):
380
- should_run_calculation, logs_calculation, grouped_measurements = _should_run_calculation(site)
394
+ should_run_calculation, grouped_measurements, logs_calculation = _should_run_calculation(site)
381
395
  result_calculation = (
382
396
  flatten([_run_calculation(site, depth_key, nodes) for depth_key, nodes in grouped_measurements.items()])
383
397
  if should_run_calculation else []
@@ -387,7 +401,7 @@ def run(site: dict):
387
401
  result_calculation + [m for m in site.get('measurements', []) if m.get('term', {}).get('@id') == TERM_ID]
388
402
  )
389
403
 
390
- should_run_rescale, logs_rescale, grouped_oc_per_ha_nodes = _should_run_rescale(oc_per_ha_nodes)
404
+ should_run_rescale, grouped_oc_per_ha_nodes, logs_rescale = _should_run_rescale(oc_per_ha_nodes)
391
405
  result_rescale = (
392
406
  [_run_rescale(site, nodes) for nodes in grouped_oc_per_ha_nodes.values()]
393
407
  if should_run_rescale else []
@@ -8,7 +8,7 @@ from copy import deepcopy
8
8
  from hestia_earth.schema import MeasurementMethodClassification
9
9
  from hestia_earth.utils.tools import non_empty_list, flatten
10
10
 
11
- from hestia_earth.models.log import logRequirements, logShouldRun, logErrorRun
11
+ from hestia_earth.models.log import logRequirements, logShouldRun, logErrorRun, log_as_table
12
12
  from hestia_earth.models.utils.measurement import _new_measurement
13
13
  from hestia_earth.models.utils.term import get_lookup_value
14
14
  from . import MODEL
@@ -20,7 +20,6 @@ REQUIREMENTS = {
20
20
  ]
21
21
  }
22
22
  }
23
-
24
23
  RETURNS = {
25
24
  "Measurement": [{
26
25
  "value": "",
@@ -30,11 +29,9 @@ RETURNS = {
30
29
  "methodClassification": "modelled using other measurements"
31
30
  }]
32
31
  }
33
-
34
32
  LOOKUPS = {
35
- "measurement": ["recommendAddingDepth", "depthSensitive"]
33
+ "measurement": "depthSensitive"
36
34
  }
37
-
38
35
  MODEL_KEY = 'soilMeasurement'
39
36
  STANDARD_DEPTHS = {(0, 30), (0, 50)}
40
37
 
@@ -114,13 +111,14 @@ def _group_measurements_by_date_method_term(measurements):
114
111
  return group_by_result
115
112
 
116
113
 
117
- def _run_harmonisation(measurements: list, needed_depths: list):
114
+ def _run_harmonisation(measurements: list) -> list:
118
115
  results = []
119
116
  grouped_measurements = _group_measurements_by_date_method_term(
120
117
  _expand_multiple_measurements(measurements)
121
118
  )
122
119
 
123
120
  for (date, method, term_id), measurements_list in grouped_measurements.items():
121
+ needed_depths = _get_depths_from_measurements(measurements_list)
124
122
  # For a target depth
125
123
  for depth_upper, depth_lower in needed_depths:
126
124
  modelled_value = _harmonise_measurements(
@@ -144,54 +142,43 @@ def _run_harmonisation(measurements: list, needed_depths: list):
144
142
  return results
145
143
 
146
144
 
147
- def _run_gap_fill_depths(measurements_missing_depths: list) -> list:
148
- return [dict(m, **{"depthUpper": 0, "depthLower": 30}) for m in measurements_missing_depths]
149
-
150
-
151
- def _get_needed_depths(site: dict) -> list:
145
+ def _get_depths_from_measurements(measurements: list) -> list:
152
146
  needed_depths = list(STANDARD_DEPTHS)
153
- for measurement in site.get("measurements", []):
147
+ for measurement in measurements:
154
148
  if (measurement.get("depthUpper"), measurement.get("depthLower")) in needed_depths:
155
149
  needed_depths.remove((int(measurement["depthUpper"]), int(measurement["depthLower"])))
156
150
 
157
151
  return needed_depths
158
152
 
159
153
 
160
- def _should_run(site: dict, model_key: str):
154
+ def _should_run(site: dict):
161
155
  # we only work with measurements with depths
162
- measurements = [m for m in site.get("measurements", []) if all([
163
- get_lookup_value(m.get("term", {}), LOOKUPS["measurement"][0], model=MODEL, model_key=model_key),
164
- m.get('value', [])
165
- ])]
166
-
156
+ measurements = site.get("measurements", [])
157
+ measurement_sensitivity = {
158
+ m.get('term', {}).get('@id'): get_lookup_value(
159
+ m.get('term', {}), LOOKUPS["measurement"], model=MODEL, model_key=MODEL_KEY
160
+ )
161
+ for m in measurements
162
+ }
167
163
  measurements_with_depths = [m for m in measurements if all([
164
+ not measurement_sensitivity[m.get("term", {}).get('@id')],
165
+ m.get('value', []),
168
166
  "depthUpper" in m,
169
- "depthLower" in m,
170
- (int(m.get("depthUpper", 0)), int(m.get("depthLower", 0))) not in STANDARD_DEPTHS
167
+ "depthLower" in m
171
168
  ])]
172
169
  has_measurements_with_depths = len(measurements_with_depths) > 0
173
170
 
174
- measurements_missing_depth_recommended = [m for m in measurements if all([
175
- "depthUpper" not in m,
176
- "depthLower" not in m,
177
- not get_lookup_value(m.get("term", {}), LOOKUPS["measurement"][1], model=MODEL, model_key=model_key)
178
- ])]
179
-
180
- logRequirements(site, model=MODEL, model_key=model_key,
181
- has_measurements_with_depths=has_measurements_with_depths,
182
- has_missing_depths=bool(measurements_missing_depth_recommended))
171
+ logRequirements(site, model=MODEL, model_key=MODEL_KEY,
172
+ measurements_depth_sensitive=log_as_table(measurement_sensitivity),
173
+ has_measurements_with_depths=has_measurements_with_depths)
183
174
 
184
- should_run = has_measurements_with_depths or bool(measurements_missing_depth_recommended)
185
- for measurement in measurements_with_depths + measurements_missing_depth_recommended:
175
+ should_run = has_measurements_with_depths
176
+ for measurement in measurements_with_depths:
186
177
  term_id = measurement.get("term", {}).get("@id", {})
187
178
  logShouldRun(site, MODEL, term_id, should_run)
188
- return should_run, measurements_with_depths, measurements_missing_depth_recommended
179
+ return should_run, measurements_with_depths
189
180
 
190
181
 
191
182
  def run(site: dict):
192
- should_run, measurements_with_depths, measurements_missing_depth = _should_run(site=site, model_key=MODEL_KEY)
193
- needed_depths = _get_needed_depths(site)
194
- return non_empty_list(flatten(
195
- _run_harmonisation(measurements=measurements_with_depths, needed_depths=needed_depths)
196
- + _run_gap_fill_depths(measurements_missing_depths=measurements_missing_depth)
197
- )) if should_run else []
183
+ should_run, measurements_with_depths = _should_run(site)
184
+ return non_empty_list(flatten(_run_harmonisation(measurements=measurements_with_depths))) if should_run else []
@@ -1,4 +1,6 @@
1
1
  from os.path import dirname, abspath
2
+ from collections.abc import Generator, Iterable
3
+ from itertools import tee
2
4
  import sys
3
5
  import datetime
4
6
  from functools import reduce
@@ -7,6 +9,7 @@ from typing import Any, Union
7
9
  from hestia_earth.schema import SchemaType
8
10
  from hestia_earth.utils.api import download_hestia
9
11
  from hestia_earth.utils.model import linked_node
12
+ from hestia_earth.utils.tools import flatten, non_empty_list
10
13
 
11
14
  from .constant import Units
12
15
 
@@ -137,3 +140,28 @@ def last_day_of_month(year: int, month: int):
137
140
  return datetime.date(int(year), 12, 31) if month == 12 else (
138
141
  datetime.date(int(year) + int(int(month) / 12), (int(month) % 12) + 1, 1) - datetime.timedelta(days=1)
139
142
  )
143
+
144
+
145
+ def flatten_args(args) -> list:
146
+ """
147
+ Flatten the input args into a single list.
148
+ """
149
+ return non_empty_list(flatten([list(arg) if is_iterable(arg) else [arg] for arg in args]))
150
+
151
+
152
+ def is_iterable(arg) -> bool:
153
+ """
154
+ Return `True` if the input arg is an instance of an `Iterable` (excluding `str` and `bytes`) or a `Generator`, else
155
+ return `False`.
156
+ """
157
+ return isinstance(arg, (Iterable, Generator)) and not isinstance(arg, (str, bytes))
158
+
159
+
160
+ def pairwise(iterable):
161
+ """
162
+ from https://docs.python.org/3.9/library/itertools.html#itertools-recipes
163
+ s -> (s0,s1), (s1,s2), (s2, s3), ...
164
+ """
165
+ a, b = tee(iterable)
166
+ next(b, None)
167
+ return zip(a, b)
@@ -1,4 +1,4 @@
1
- from .site import WATER_TYPES
1
+ from .site import WATER_TYPES, valid_site_type as site_valid_site_type
2
2
 
3
3
 
4
- def valid_site_type(cycle: dict): return cycle.get('site', {}).get('siteType') in WATER_TYPES
4
+ def valid_site_type(cycle: dict): return site_valid_site_type(cycle.get('site', {}), WATER_TYPES)