Skip to content

Commit

Permalink
MOBT-94: Weather symbol changes to accommodate multiple optional nodes (
Browse files Browse the repository at this point in the history
#1585)

* Add ECC bounds for lwe_precipitation_rate_max.

* Weather symbols code modifies tree to account for allowed missing diagnostics.

Approach modified such that optional missing diagnostics are now accounted for by modifying the tree nodes and then calculating the routes, rather than just calculating the routes to skip the missing nodes. Unit tests added for this new approach, and removed for the old approach.

Work in progress changes to weathersymbols to modify tree when nodes are missing, rather than just route around them.

* Black.

* Modify test to cover if_true as alternative node.

* Most review changes.

* Unit tests modified.

* Review changes.

* Remove unused import.

* Clarify comment.

* Add back test routing through the if_true option.
  • Loading branch information
bayliffe authored Oct 19, 2021
1 parent ca028e3 commit 1cd0489
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 98 deletions.
1 change: 1 addition & 0 deletions improver/ensemble_copula_coupling/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
# Precipitation rate
"lwe_precipitation_rate": Bounds((0, 128.0), "mm h-1"),
"lwe_precipitation_rate_in_vicinity": Bounds((0, 128.0), "mm h-1"),
"lwe_precipitation_rate_max": Bounds((0, 128.0), "mm h-1"),
"lwe_sleetfall_rate": Bounds((0, 128.0), "mm h-1"),
"lwe_snowfall_rate": Bounds((0, 128.0), "mm h-1"),
"lwe_snowfall_rate_in_vicinity": Bounds((0, 128.0), "mm h-1"),
Expand Down
88 changes: 51 additions & 37 deletions improver/wxcode/weather_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

import copy
import operator
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union

import iris
import numpy as np
Expand Down Expand Up @@ -135,7 +135,7 @@ def __repr__(self) -> str:
"""Represent the configured plugin instance as a string."""
return "<WeatherSymbols start_node={}>".format(self.start_node)

def check_input_cubes(self, cubes: CubeList) -> Optional[Dict[str, Any]]:
def check_input_cubes(self, cubes: CubeList) -> Optional[List[str]]:
"""
Check that the input cubes contain all the diagnostics and thresholds
required by the decision tree. Sets self.coord_named_threshold to
Expand All @@ -148,9 +148,9 @@ def check_input_cubes(self, cubes: CubeList) -> Optional[Dict[str, Any]]:
A CubeList containing the input diagnostic cubes.
Returns:
A dictionary of (keyword) nodes names where the diagnostic
data is missing and (values) node associated with
if_diagnostic_missing.
A list of node names where the diagnostic data is missing and
this is indicated as allowed by the presence of the if_diagnostic_missing
key.
Raises:
IOError:
Expand All @@ -160,7 +160,7 @@ def check_input_cubes(self, cubes: CubeList) -> Optional[Dict[str, Any]]:
# Check that all cubes are valid at or over the same periods
self.check_coincidence(cubes)

optional_node_data_missing = {}
optional_node_data_missing = []
missing_data = []
for key, query in self.queries.items():
diagnostics = get_parameter_names(
Expand All @@ -178,9 +178,7 @@ def check_input_cubes(self, cubes: CubeList) -> Optional[Dict[str, Any]]:
matched_cube = cubes.extract(test_condition)
if not matched_cube:
if "if_diagnostic_missing" in query:
optional_node_data_missing.update(
{key: query[query["if_diagnostic_missing"]]}
)
optional_node_data_missing.append(key)
else:
missing_data.append([diagnostic, threshold, condition])
continue
Expand Down Expand Up @@ -341,7 +339,6 @@ def create_condition_chain(self, test_conditions: Dict) -> List:
test_conditions["probability_thresholds"],
test_conditions["diagnostic_thresholds"],
):

loop += 1

if isinstance(diagnostic, list):
Expand Down Expand Up @@ -424,13 +421,40 @@ def construct_extract_constraint(
constraint = iris.Constraint(name=diagnostic, **kw_dict)
return constraint

def remove_optional_missing(self, optional_node_data_missing: List[str]):
"""
Some decision tree nodes are optional and have an "if_diagnostic_missing"
key to enable passage through the tree in the absence of the required
input diagnostic. This code modifies the tree in the following ways:
- Rewrites the decision tree to skip the missing nodes by connecting
nodes that proceed them to the node targetted by "if_diagnostic_missing"
- If the node(s) missing are those at the start of the decision
tree, the start_node is modified to find the first available node.
Args:
optional_node_data_missing:
List of node names for which data is missing but for which this
is allowed.
"""
for missing in optional_node_data_missing:

# Get the name of the alternative node to bypass the missing one
target = self.queries[missing]["if_diagnostic_missing"]
alternative = self.queries[missing][target]

for node, query in self.queries.items():
if query["if_true"] == missing:
query["if_true"] = alternative
if query["if_false"] == missing:
query["if_false"] = alternative

if self.start_node == missing:
self.start_node = alternative

@staticmethod
def find_all_routes(
graph: Dict,
start: str,
end: int,
omit_nodes: Optional[Dict] = None,
route: Optional[List[str]] = None,
graph: Dict, start: str, end: int, route: Optional[List[str]] = None,
) -> List[str]:
"""
Function to trace all routes through the decision tree.
Expand All @@ -441,13 +465,9 @@ def find_all_routes(
e.g. {<node_name>: [<if_true_name>, <if_false_name>]}
start:
The node name of the tree root (currently always
heavy_precipitation).
lightning).
end:
The weather symbol code to which we are tracing all routes.
omit_nodes:
A dictionary of (keyword) nodes names where the diagnostic
data is missing and (values) node associated with
if_diagnostic_missing.
route:
A list of node names found so far.
Expand All @@ -462,13 +482,6 @@ def find_all_routes(
if route is None:
route = []

if omit_nodes:
start_not_valid = True
while start_not_valid:
if start in omit_nodes:
start = omit_nodes[start]
else:
start_not_valid = False
route = route + [start]
if start == end:
return [route]
Expand All @@ -479,7 +492,7 @@ def find_all_routes(
for node in graph[start]:
if node not in route:
newroutes = WeatherSymbols.find_all_routes(
graph, node, end, omit_nodes=omit_nodes, route=route
graph, node, end, route=route
)
routes.extend(newroutes)
return routes
Expand Down Expand Up @@ -699,6 +712,11 @@ def process(self, cubes: CubeList) -> Cube:
"""
# Check input cubes contain required data
optional_node_data_missing = self.check_input_cubes(cubes)

# Reroute the decision tree around missing optional nodes
if optional_node_data_missing is not None:
self.remove_optional_missing(optional_node_data_missing)

# Construct graph nodes dictionary
graph = {
key: [self.queries[key]["if_true"], self.queries[key]["if_false"]]
Expand All @@ -713,18 +731,12 @@ def process(self, cubes: CubeList) -> Cube:
# Create symbol cube
symbols = self.create_symbol_cube(cubes)
# Loop over possible symbols
for symbol_code in defined_symbols:

for symbol_code in defined_symbols:
# In current decision tree
# start node is heavy_precipitation
routes = self.find_all_routes(
graph,
self.start_node,
symbol_code,
omit_nodes=optional_node_data_missing,
)
# start node is lightning
routes = self.find_all_routes(graph, self.start_node, symbol_code,)
# Loop over possible routes from root to leaf

for route in routes:
conditions = []
for i_node in range(len(route) - 1):
Expand All @@ -736,6 +748,7 @@ def process(self, cubes: CubeList) -> Cube:
next_node = symbol_code

if current["if_false"] == next_node:

(
current["threshold_condition"],
current["condition_combination"],
Expand All @@ -748,6 +761,7 @@ def process(self, cubes: CubeList) -> Cube:
symbols.data[
np.ma.where(self.evaluate_condition_chain(cubes, test_chain))
] = symbol_code

# Update symbols for day or night.
symbols = update_daynight(symbols)
return symbols
44 changes: 41 additions & 3 deletions improver_tests/wxcode/wxcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ def wxcode_decision_tree() -> Dict[str, Dict[str, Any]]:
"""
queries = {
"lightning": {
"if_true": "lightning_cloud",
"if_false": "heavy_precipitation",
"if_true": "lightning_shower",
"if_false": "hail",
"if_diagnostic_missing": "if_false",
"probability_thresholds": [0.3],
"threshold_condition": ">=",
Expand All @@ -110,7 +110,7 @@ def wxcode_decision_tree() -> Dict[str, Dict[str, Any]]:
"diagnostic_thresholds": [[0.0, "m-2"]],
"diagnostic_conditions": ["above"],
},
"lightning_cloud": {
"lightning_shower": {
"if_true": 29,
"if_false": 30,
"probability_thresholds": [0.5],
Expand All @@ -120,9 +120,46 @@ def wxcode_decision_tree() -> Dict[str, Dict[str, Any]]:
"diagnostic_thresholds": [[1.0, 1]],
"diagnostic_conditions": ["above"],
},
"hail": {
"if_true": "hail_precip",
"if_false": "heavy_precipitation",
"if_diagnostic_missing": "if_false",
"probability_thresholds": [0.5],
"threshold_condition": ">=",
"condition_combination": "",
"diagnostic_fields": [
"probability_of_lwe_graupel_and_hail_fall_rate_in_vicinity_above_threshold"
],
"diagnostic_thresholds": [[0.0, "mm hr-1"]],
"diagnostic_conditions": ["above"],
},
"hail_precip": {
"if_true": "hail_shower",
"if_false": "heavy_precipitation",
"if_diagnostic_missing": "if_false",
"probability_thresholds": [0.5],
"threshold_condition": ">=",
"condition_combination": "",
"diagnostic_fields": [
"probability_of_lwe_precipitation_rate_max_above_threshold"
],
"diagnostic_thresholds": [[1.0, "mm hr-1"]],
"diagnostic_conditions": ["above"],
},
"hail_shower": {
"if_true": 20,
"if_false": 21,
"probability_thresholds": [0.5],
"threshold_condition": ">=",
"condition_combination": "",
"diagnostic_fields": ["probability_of_shower_condition_above_threshold"],
"diagnostic_thresholds": [[1.0, 1]],
"diagnostic_conditions": ["above"],
},
"heavy_precipitation": {
"if_true": "heavy_precipitation_cloud",
"if_false": "precipitation_in_vicinity",
"if_diagnostic_missing": "if_false",
"probability_thresholds": [0.5],
"threshold_condition": ">=",
"condition_combination": "",
Expand All @@ -135,6 +172,7 @@ def wxcode_decision_tree() -> Dict[str, Dict[str, Any]]:
"heavy_precipitation_cloud": {
"if_true": "heavy_snow_shower",
"if_false": "heavy_snow_continuous",
"if_diagnostic_missing": "if_true",
"probability_thresholds": [0.5],
"threshold_condition": ">=",
"condition_combination": "",
Expand Down
Loading

0 comments on commit 1cd0489

Please sign in to comment.