Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dei 229 depth average select z or sigma from inputfile #121

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1c5e15b
add file for depth averaging with sigma layers
mKlapwijk Aug 1, 2024
bc77fee
working version for sigma layers. DOES not work for z
mKlapwijk Aug 1, 2024
dd98962
add test 18, correct paths for test 17
mKlapwijk Aug 2, 2024
782479c
rename test 17 reference file
mKlapwijk Aug 2, 2024
1e58076
flake problems
mKlapwijk Aug 2, 2024
1401087
more flake problems
mKlapwijk Aug 2, 2024
2dcd52a
and more flake8
mKlapwijk Aug 2, 2024
66886c2
adding print statements in an attempt to get both to work
mKlapwijk Aug 2, 2024
e1d0e44
Merge branch 'feature/DEI-229-depth-averaged-rules-sigma-models' into…
mKlapwijk Aug 2, 2024
831f072
Go for the much simpler approach. Let the user specify the layer type
mKlapwijk Aug 2, 2024
6f3005e
Update tests to specify layer type
mKlapwijk Aug 2, 2024
199c43d
add reference file for test 18
mKlapwijk Aug 2, 2024
5d285f7
add test for parser error
mKlapwijk Aug 2, 2024
56c1382
restore accidentally committed file
mKlapwijk Aug 2, 2024
f1659c7
line too long
mKlapwijk Aug 2, 2024
8c0e655
remove import
mKlapwijk Aug 2, 2024
a357e09
update documentation
mKlapwijk Aug 2, 2024
e36e15e
fix unit tests
mKlapwijk Aug 2, 2024
ca9f8f0
add layer type to depth average rule data
mKlapwijk Aug 5, 2024
c3c04b7
remove check layer function from depth average rule. Use layer type v…
mKlapwijk Aug 5, 2024
4b4fec8
fix unit tests
mKlapwijk Aug 5, 2024
c748e01
typo
mKlapwijk Aug 5, 2024
6371010
move functions out of class
mKlapwijk Aug 5, 2024
72240b2
add layer type to creation
mKlapwijk Aug 5, 2024
fb84852
remove import
mKlapwijk Aug 5, 2024
ac30794
remove unnecessary if's
mKlapwijk Aug 5, 2024
e01fe92
remove imports. move function out of class
mKlapwijk Aug 5, 2024
3e67a26
fix flake
mKlapwijk Aug 5, 2024
bde07c8
lsit -> List
mKlapwijk Aug 5, 2024
d5a47e3
final flake
mKlapwijk Aug 5, 2024
7b21ffb
fix the error when a variable name is remapped
mKlapwijk Aug 6, 2024
5ce35eb
add check on illegal suffixes in remapping
mKlapwijk Aug 6, 2024
537d0a5
fix test
mKlapwijk Aug 6, 2024
931dc71
remove unneeded imports
mKlapwijk Aug 6, 2024
2c877ab
disable pylint warning
mKlapwijk Aug 6, 2024
2116bfd
extend error message
mKlapwijk Aug 6, 2024
11fa65d
Merge branch 'main' into feature/DEI-229-depth-average-select-z-or-si…
mKlapwijk Aug 6, 2024
20ac68d
disable pylint warning
mKlapwijk Aug 6, 2024
2ff39d8
Revert "disable pylint warning"
mKlapwijk Aug 6, 2024
755967f
remove comment
mKlapwijk Aug 6, 2024
8b78847
add return of unknown suffix
mKlapwijk Aug 6, 2024
b1dad4f
disable pylint warnings
mKlapwijk Aug 6, 2024
be1f265
correct ignore message
mKlapwijk Aug 6, 2024
118fe49
Merge branch 'main' into feature/DEI-229-depth-average-select-z-or-si…
mKlapwijk Aug 7, 2024
bb947dc
update depth average rule documentation
mKlapwijk Aug 7, 2024
d4fc2ad
add note on remapping several variables
mKlapwijk Aug 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions decoimpact/business/entities/rule_based_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from decoimpact.business.entities.i_model import IModel, ModelStatus
from decoimpact.business.entities.rule_processor import RuleProcessor
from decoimpact.business.entities.rules.i_rule import IRule
from decoimpact.crosscutting.delft3d_specific_data import delft3d_specific_names
from decoimpact.crosscutting.i_logger import ILogger


Expand Down Expand Up @@ -153,6 +154,7 @@ def _make_output_variables_list(self) -> list:

return _lu.remove_duplicates_from_list(all_vars)

# pylint: disable=too-many-locals
def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:
"""Checks if the provided mappings are valid.

Expand All @@ -169,6 +171,7 @@ def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:
for ds in self._input_datasets
]
)
self._check_keys_with_suffixes(mappings, delft3d_specific_names)

valid = True

Expand Down Expand Up @@ -198,10 +201,17 @@ def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:

rule_inputs = self._get_direct_rule_inputs(rule_names)

for dataset in self._input_datasets:
dummy_var_name = _du.get_dummy_variable_in_ugrid(dataset)

# check for missing rule inputs
for rule_name, rule_input in rule_inputs.items():
needed_rule_inputs = _lu.remove_duplicates_from_list(rule_input)
rule_input_vars = input_vars + list(mappings.values())
needed_rule_inputs = _du.extend_to_full_name(
needed_rule_inputs,
dummy_var_name
)
missing_rule_inputs = _lu.items_not_in(needed_rule_inputs, rule_input_vars)
if len(missing_rule_inputs) > 0:
logger.log_error(
Expand Down Expand Up @@ -239,3 +249,19 @@ def _extend_names(self, dummy_variable_name: str):
rule.input_variable_names,
dummy_variable_name
)

def _check_keys_with_suffixes(self, dictionary, suffixes):
"""
Checks if any key in the dictionary ends with provided suffixes.
Raise eror if that occurs.

Args:
dictionary (dict) : The dictionary to check.
suffixes (List[str]): List of suffixes to check against.

"""
for key in dictionary:
if any(key.endswith(suffix) for suffix in suffixes):
raise ValueError(f"Remapping variables ending with"
f" {delft3d_specific_names} is not"
f" allowed.")
1 change: 1 addition & 0 deletions decoimpact/business/entities/rule_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def initialize(self, logger: ILogger) -> bool:
inputs = _lu.flatten_list(
[_du.list_vars(self._input_dataset), _du.list_coords(self._input_dataset)]
)

tree, success = self._create_rule_sets(inputs, self._rules, [], logger)
if success:
self._processing_list = tree
Expand Down
69 changes: 49 additions & 20 deletions decoimpact/business/entities/rules/depth_average_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from decoimpact.business.entities.rules.rule_base import RuleBase
from decoimpact.crosscutting.delft3d_specific_data import (
BED_LEVEL_SUFFIX,
INTERFACES_SIGMA_SUFFIX,
INTERFACES_Z_SUFFIX,
WATER_LEVEL_SUFFIX,
)
Expand All @@ -29,6 +30,11 @@
class DepthAverageRule(RuleBase, IMultiArrayBasedRule):
"""Implementation for the depth average rule"""

def __init__(self, name: str, input_variable_names: List[str], layer_type: str):
super().__init__(name, input_variable_names)
self._layer_type = layer_type

# pylint: disable=too-many-locals
def execute(
self, value_arrays: Dict[str, _xr.DataArray], logger: ILogger
) -> _xr.DataArray:
Expand All @@ -45,12 +51,13 @@ def execute(
# but the name of the key is given by the user, and is unknown here, so
# just use the first value.
variables = next(iter(value_arrays.values()))
interface_suffix = _get_layer_suffix(self._layer_type, logger)

bed_level_values = self._extract_variable_based_on_suffix(
bed_level_values = _extract_variable_based_on_suffix(
value_arrays, BED_LEVEL_SUFFIX)
depths_interfaces = self._extract_variable_based_on_suffix(
value_arrays, INTERFACES_Z_SUFFIX)
water_level_values = self._extract_variable_based_on_suffix(
depths_interfaces = _extract_variable_based_on_suffix(
value_arrays, interface_suffix)
water_level_values = _extract_variable_based_on_suffix(
value_arrays, WATER_LEVEL_SUFFIX)

# Get the dimension names for the interfaces and for the layers
Expand Down Expand Up @@ -109,20 +116,42 @@ def execute(
dim=dim_layer_name
)

def _extract_variable_based_on_suffix(
self,
value_arrays: Dict[str, _xr.DataArray],
suffix: str
) -> List:
"""Extract the values from the XArray dataset based on the name
suffixes by matching the name, irrespective of the dummy name prefix.

Args:
value_array (DataArray): Values
suffix (str) : Suffix of the name

Returns:
values (List[str]): Values based on prefix + suffix name
"""
variable = [value_arrays[name] for name in value_arrays if suffix in name][0]
return variable
def _extract_variable_based_on_suffix(
value_arrays: Dict[str, _xr.DataArray],
suffix: str
) -> List:
"""Extract the values from the XArray dataset based on the name
suffixes by matching the name, irrespective of the dummy name prefix.

Args:
value_array (DataArray): Values
suffix (str) : Suffix of the name

Returns:
values (List[str]): Values based on prefix + suffix name
"""
return [value_arrays[name] for name in value_arrays if suffix in name][0]


def _get_layer_suffix(
layer_type: str,
logger: ILogger
):
"""Get the interface suffix depending on whether the model is a sigma or z
layer model. Give error if the interface suffix cannot be determined.

Args:
value_array (DataArray): Values

Returns:
layer_type (str): sigma or z
"""
if layer_type.lower() == "sigma":
return INTERFACES_SIGMA_SUFFIX
if layer_type.lower() == "z":
return INTERFACES_Z_SUFFIX
logger.log_error(f"Layer type {layer_type} unknown. Allowed layer "
"type: z or sigma. Interface "
"variable could not be determined.")
return "_unknown"
1 change: 1 addition & 0 deletions decoimpact/business/workflow/model_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def _create_rule(rule_data: IRuleData) -> IRule:
rule = DepthAverageRule(
rule_data.name,
rule_data.input_variables,
rule_data.layer_type,
)
elif isinstance(rule_data, ILayerFilterRuleData):
rule = LayerFilterRule(
Expand Down
5 changes: 5 additions & 0 deletions decoimpact/data/api/i_depth_average_rule_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ class IDepthAverageRuleData(IRuleData, ABC):
@abstractmethod
def input_variables(self) -> List[str]:
"""List with input variable name and standard depth name"""

@property
@abstractmethod
def layer_type(self) -> str:
"""Layer type of the model (z or sigma)"""
8 changes: 8 additions & 0 deletions decoimpact/data/entities/depth_average_rule_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""

from typing import List

from decoimpact.data.api.i_depth_average_rule_data import IDepthAverageRuleData
from decoimpact.data.entities.rule_data import RuleData

Expand All @@ -24,11 +25,18 @@ def __init__(
self,
name: str,
input_variables: List[str],
layer_type: str,
):
super().__init__(name)
self._input_variables = input_variables
self._layer_type = layer_type

@property
def input_variables(self) -> List[str]:
"""List with input variables"""
return self._input_variables

@property
def layer_type(self) -> str:
"""Layer type of the model (z or sigma)"""
return self._layer_type
27 changes: 25 additions & 2 deletions decoimpact/data/parsers/parser_depth_average_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from decoimpact.crosscutting.delft3d_specific_data import (
BED_LEVEL_SUFFIX,
INTERFACES_SIGMA_SUFFIX,
INTERFACES_Z_SUFFIX,
WATER_LEVEL_SUFFIX,
)
Expand Down Expand Up @@ -41,19 +42,41 @@ def parse_dict(self, dictionary: Dict[str, Any], logger: ILogger) -> IRuleData:
RuleBase: Rule based on the provided data
"""
name: str = get_dict_element("name", dictionary)
layer_type: str = get_dict_element("layer_type", dictionary)
interface_suffix = _obtain_interface_suffix(layer_type)

input_variable_names: List[str] = [
get_dict_element("input_variable", dictionary),
INTERFACES_Z_SUFFIX,
interface_suffix,
WATER_LEVEL_SUFFIX,
BED_LEVEL_SUFFIX,
]

output_variable_name: str = get_dict_element("output_variable", dictionary)
description: str = get_dict_element("description", dictionary, False) or ""

rule_data = DepthAverageRuleData(name, input_variable_names)
rule_data = DepthAverageRuleData(name, input_variable_names, layer_type)

rule_data.output_variable = output_variable_name
rule_data.description = description

return rule_data


def _obtain_interface_suffix(layer_type: str):
"""Obtain the interface variable based on the layer_type specified.
Give an error if layer_type is not recognised

Args:
layer_type (str): z or sigma layers

Returns:
Suffix for z or sigma layers based on Delft3D
defined suffixes
"""
if layer_type.lower() == 'z':
return INTERFACES_Z_SUFFIX
if layer_type.lower() == 'sigma':
return INTERFACES_SIGMA_SUFFIX
raise NotImplementedError(f"Layer_type '{layer_type}' is not recognized. "
f"Supported options are 'z' and 'sigma'.")
2 changes: 2 additions & 0 deletions docs/manual/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ output-data:

In the input data the variables that are present in the input data provided through “filename” are selected for use. It is possible to filter the input data by providing a start date or end date (format: "dd-mm-yyyy"); this is optional. The variables that are used can be selected under “variable_mapping”. Here you are also able to rename variables as the name used for storage is often cryptic.

Note: remapping the variables 'mesh2d_interface_z', 'mesh2d_interface_sigma', 'mesh2d_flowelem_bl and 'mesh2d_s1' is currently not supported to enable depth-averaging. This will be remedied in a future version. For now a work-around is to use a multiply rule with a multiplication factor of 1. This will in effect create a new variable, to which the remapped name can be assigned as output.

At output data the location where the output file needs to be written can be provided through “filename”. In this output file only variables that have been used from the input data and variables that have been created in the model are stored. It is possible to reduce the file size with the optional parameter "save_only_variables", which can take the name of one or several variables.
The model needs at least one rule under “rules” to execute.

Expand Down
12 changes: 7 additions & 5 deletions docs/manual/rules/depth_average_rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

### Depth average rule

```
Expand All @@ -7,14 +6,17 @@ FORMAT
name: <name_of_rule_in_text>
description: <description_of_rule_in_text>
input_variable: <one_input_variable_name>
layer_type: <layer_type_used_in_dataset>
output_variable: <one_output_variable_name>
```

The depth average rule allows for an averaging over depth using the weighted values according to a mesh with z-layers. The input file must include a variable called 'mesh2d_interface_z' over which the the input variable will be averaged. The input_variable will be a 2D/3D variable, with or without time axis. The output_variable has the same dimensions, excluding the dimension for the depth, as it will be represented as one averaged value per cell.
The depth average rule allows for an averaging over depth using the weighted values according to a mesh with z- or sigma-layers. The current implementation only supports input netCDF files generated by D-Hydro. The input file must include a variable called 'mesh2d_interface_z' or 'mesh2d_interface_sigma' over which the input variable will be averaged. Also two variables specifying the bedlevel and water level, 'mesh2d_flowelem_bl' and 'mesh2d_s1' are needed. The input_variable will be a 2D/3D variable, with or without time axis. The output_variable has the same dimensions, excluding the dimension for the depth, as it will be represented as one averaged value per cell.

Note: remapping the variables 'mesh2d_interface_z', 'mesh2d_interface_sigma', 'mesh2d_flowelem_bl and 'mesh2d_s1' in combination with the depth average rule is currently not supported. Combined z-sigma layers are also not supported.

An explanation of how the depth rule works is shown in the example below.

![Example depth average rule](../../assets/images/3_depth_average.png "An example of a simplified grid with Z-layers. This model has 6 faces, 4 layers and 2 timesteps.")
![Example depth average rule](../assets/images/3_depth_average.png "An example of a simplified grid with Z-layers. This model has 6 faces, 4 layers and 2 timesteps.")

The image shows a simplified model with the following dimensions:
- mesh2d_nFaces = 6 (number of faces)
Expand Down Expand Up @@ -95,6 +97,6 @@ Below is an example of an input_file for the depth average rule:
name: test depth average
description: Test depth average
input_variable: salinity
layer_type : sigma
output_variable: average_salinity
```

```
14 changes: 9 additions & 5 deletions tests/business/entities/rules/test_depth_average_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import numpy as _np
import pytest
import xarray as _xr
from tomlkit import value

from decoimpact.business.entities.rules.depth_average_rule import DepthAverageRule
from decoimpact.crosscutting.i_logger import ILogger
Expand All @@ -24,14 +23,15 @@ def test_create_depth_average_rule_with_defaults():
"""Test creating a depth average rule with defaults"""

# Arrange & Act
rule = DepthAverageRule("test_rule_name", ["foo", "hello"])
rule = DepthAverageRule("test_rule_name", ["foo", "hello"],"z")

# Assert
assert isinstance(rule, DepthAverageRule)
assert rule.name == "test_rule_name"
assert rule.description == ""
assert rule.input_variable_names == ["foo", "hello"]
assert rule.output_variable_name == "output"
assert rule._layer_type == "z"


def test_no_validate_error_with_correct_rule():
Expand All @@ -41,6 +41,7 @@ def test_no_validate_error_with_correct_rule():
rule = DepthAverageRule(
"test_rule_name",
["foo", "hello"],
"z",
)

# Assert
Expand Down Expand Up @@ -95,7 +96,8 @@ def test_depth_average_rule(
logger = Mock(ILogger)
rule = DepthAverageRule(
name="test",
input_variable_names=["foo"],
input_variable_names=["foo", "mesh2d_interface_z"],
layer_type="z",
)

# Create dataset
Expand Down Expand Up @@ -126,11 +128,13 @@ def test_depth_average_rule(


def test_dimension_error():
"""If the number of interfaces > number of layers + 1. Give an error, no calculation is possible"""
"""If the number of interfaces > number of layers + 1. Give an error, no
calculation is possible"""
logger = Mock(ILogger)
rule = DepthAverageRule(
name="test",
input_variable_names=["foo"],
input_variable_names=["foo", "mesh2d_interface_z"],
layer_type="z",
)

# Create dataset
Expand Down
1 change: 1 addition & 0 deletions tests/business/entities/test_rule_based_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def test_validation_of_rule_based_model():
logger = Mock(ILogger)

dataset["test"] = _xr.DataArray([32, 94, 9])
dataset["test"].attrs = {"cf_role": "mesh_topology"}

rule.input_variable_names = ["input"]
rule.output_variable_name = "output"
Expand Down
3 changes: 2 additions & 1 deletion tests/data/entities/test_depth_average_rule_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ def test_depth_average_rule_data_creation_logic():
to correctly initialize itself during creation"""

# Act
data = DepthAverageRuleData("test_name", "input1")
data = DepthAverageRuleData("test_name", "input1", "z")

# Assert

assert isinstance(data, IRuleData)
assert data.input_variables == "input1"
assert data.layer_type == "z"
Loading
Loading