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

Dei 185 depth average rule #114

Merged
merged 75 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
d5e9cf6
feat[DEI-185]: create depth average rule
wschoonveld Jun 19, 2024
5a23242
add todo and parser
CindyvdVries Jun 19, 2024
67bec0a
add data
CindyvdVries Jun 19, 2024
fc9051d
Update template_input.yaml
CindyvdVries Jun 19, 2024
8061706
add to modelbuilder
CindyvdVries Jun 19, 2024
c8521fb
DEI-185: depth_average_rule: calculate layer heights
wschoonveld Jun 19, 2024
74709fc
feat[DEI-185]: depth_average_rule: calculate depth average
wschoonveld Jun 20, 2024
6de8cc4
feat[DEI-185]: correct layer data and parser and fix flake issues
wschoonveld Jun 20, 2024
69ff442
feat[DEI-185]: use enumerate (fix flake8)
wschoonveld Jun 20, 2024
bd9a9a9
feat[DEI-185]: clean up comments
wschoonveld Jun 20, 2024
24bdabb
feat[DEI-185]: flake8
wschoonveld Jun 21, 2024
22f82c7
feat[DEI-185]: pylint warning
wschoonveld Jun 21, 2024
b3381e3
Merge branch 'DEI-185-depth-average-rule' of https://github.com/Delta…
CindyvdVries Jun 21, 2024
6868a9f
feat[DEI-185]: rename depth_average_layer_data to depth_average_rule_…
wschoonveld Jun 21, 2024
4aee6cd
feat[DEI-185]: rule type implementation
wschoonveld Jun 21, 2024
c7297ff
Merge branch 'DEI-185-depth-average-rule' of https://github.com/Delta…
CindyvdVries Jun 21, 2024
6620a89
Complete workflow for depth averaged rule
CindyvdVries Jun 24, 2024
5292e7c
first implementation depth averaged rule
CindyvdVries Jun 24, 2024
ec78db9
correct calculation + clean up
CindyvdVries Jun 25, 2024
5f2fa1e
fix typo
mKlapwijk Jun 26, 2024
86f340c
typo. Add test for depth averaged rule
mKlapwijk Jun 26, 2024
aad71cd
correct validation test of depth average rule
mKlapwijk Jun 26, 2024
dd59628
remove trailing spaces
mKlapwijk Jun 26, 2024
22cac02
typo
mKlapwijk Jun 26, 2024
7f57b9b
add test
mKlapwijk Jun 26, 2024
fb5e0bc
feat[DEI-185]: changes in depth average rule
wschoonveld Jun 26, 2024
cfed894
Merge branch 'DEI-185-depth-average-rule' of https://github.com/Delta…
wschoonveld Jun 26, 2024
e7a5b7b
feat[DEI-185]: changes in the depth average rule
wschoonveld Jun 26, 2024
1f7baab
feat[DEI-185]: clean up print statements
wschoonveld Jun 26, 2024
e480591
feat[DEI-185]: enable error again to check on dummy variable
wschoonveld Jun 26, 2024
52d6e6e
feat[DEI-185]: add water level and bed level to be able to performn c…
wschoonveld Jun 26, 2024
cf1c1a0
correction for bed level and waterlevel
CindyvdVries Jun 27, 2024
d1c398f
add unit test
CindyvdVries Jun 27, 2024
f068ceb
clean up and comments and typo
CindyvdVries Jun 27, 2024
78f3e6e
unit test parser
CindyvdVries Jun 27, 2024
ab9d6d8
correct nan values where it should be 0
CindyvdVries Jun 27, 2024
542c7f8
set value back to nan
CindyvdVries Jun 28, 2024
93278d0
feat[DEI-185]: use normal multiply and count on NaN
wschoonveld Jun 28, 2024
fd783bd
feat[DEI-185]: add simpler unit test
wschoonveld Jun 28, 2024
7ef1fa7
feat[DEI-185]: include data in unit test to prevent mix-up
wschoonveld Jun 28, 2024
bd5a70b
feat[DEI-185]: change complex unit test: use tolerance level
wschoonveld Jun 28, 2024
59a2e4b
feat[DEI-185]: remove useless init (pylint)
wschoonveld Jun 28, 2024
aa7bf44
feat[DEI-185]: remove List (pylint)
wschoonveld Jun 28, 2024
cd60128
add data object test
CindyvdVries Jun 28, 2024
4c44a2a
parametrize test
CindyvdVries Jun 28, 2024
5c8ac46
add warning when no dimension is available
CindyvdVries Jun 28, 2024
2dabb88
Let user know explicit which rule is missing variables
CindyvdVries Jul 1, 2024
e45aa83
fix rule_based_model
CindyvdVries Jul 1, 2024
aebce22
use dims from variables instead of hardcoded names
CindyvdVries Jul 1, 2024
d41e96e
add warning and unit test if dimension mismatch
CindyvdVries Jul 1, 2024
1c062c0
feat[DEI-185]: reduce number of local variables (pylint)
wschoonveld Jul 1, 2024
4ee1bc5
removing duplicate code
CindyvdVries Jul 1, 2024
3b64234
reduce branches (pylint)
CindyvdVries Jul 1, 2024
4ec7f4e
reduce number of local variables
CindyvdVries Jul 1, 2024
c7f4df7
Update rule_based_model.py
CindyvdVries Jul 1, 2024
265da25
fix pylint
CindyvdVries Jul 1, 2024
bd58cfc
Update depth_average_rule.py
CindyvdVries Jul 1, 2024
bee7944
Update rule_based_model.py
CindyvdVries Jul 1, 2024
942e78f
Update model_builder.py
CindyvdVries Jul 1, 2024
51d71c6
update with review comments
CindyvdVries Jul 2, 2024
244516b
Update model_builder.py
CindyvdVries Jul 2, 2024
795cacf
Update model_builder.py
CindyvdVries Jul 2, 2024
f084d2e
added documentation + extra example test
CindyvdVries Jul 2, 2024
057be2a
making generic variable list
CindyvdVries Jul 2, 2024
070ab52
pylint....
CindyvdVries Jul 2, 2024
ded9a47
forgot to rename the used variables
CindyvdVries Jul 2, 2024
acf203a
Merge branch 'main' into DEI-185-depth-average-rule
CindyvdVries Jul 2, 2024
14ed93b
improve readability of the documentation
mKlapwijk Jul 3, 2024
a22e324
add space
mKlapwijk Jul 3, 2024
8515b69
remove pandoc test
mKlapwijk Jul 3, 2024
f44f615
update manual and make latex code visible
CindyvdVries Jul 4, 2024
43f3555
Update 3_depth_average.png
CindyvdVries Jul 4, 2024
665f4e1
update test cases for complex situations
CindyvdVries Jul 4, 2024
e8b62d7
Dei 217 depth average test case (#115)
CindyvdVries Jul 5, 2024
a423fe3
include coords
CindyvdVries Jul 5, 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
59 changes: 35 additions & 24 deletions decoimpact/business/entities/rule_based_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

"""

from typing import List, Optional
from typing import Dict, List, Optional

import xarray as _xr

Expand Down Expand Up @@ -104,9 +104,8 @@ def initialize(self, logger: ILogger) -> None:
self._input_datasets, self._make_output_variables_list(), self._mappings
)
self._rule_processor = RuleProcessor(self._rules, self._output_dataset)
success = self._rule_processor.initialize(logger)

if not success:
if not self._rule_processor.initialize(logger):
logger.log_error("Initialization failed.")

def execute(self, logger: ILogger) -> None:
Expand Down Expand Up @@ -142,8 +141,11 @@ def _make_output_variables_list(self) -> list:
var_list = _du.get_dummy_and_dependent_var_list(dataset)

mapping_keys = list((self._mappings or {}).keys())
rule_names = [rule.name for rule in self._rules]
all_inputs = self._get_direct_rule_inputs(rule_names)
all_input_variables = _lu.flatten_list(list(all_inputs.values()))

all_vars = var_list + mapping_keys + self._get_direct_rule_inputs()
all_vars = var_list + mapping_keys + all_input_variables
return _lu.remove_duplicates_from_list(all_vars)

def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:
Expand All @@ -157,7 +159,10 @@ def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:
bool: if mappings are valid
"""
input_vars = _lu.flatten_list(
[_du.list_vars(ds) for ds in self._input_datasets]
[
_lu.flatten_list([_du.list_vars(ds), _du.list_coords(ds)])
for ds in self._input_datasets
]
)

valid = True
Expand All @@ -174,8 +179,7 @@ def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:
valid = False

# check for duplicates that will be created because of mapping
mapping_vars_created = list(mappings.values())
duplicates_created = _lu.items_in(mapping_vars_created, input_vars)
duplicates_created = _lu.items_in(list(mappings.values()), input_vars)

if len(duplicates_created) > 0:
logger.log_error(
Expand All @@ -185,31 +189,38 @@ def _validate_mappings(self, mappings: dict[str, str], logger: ILogger) -> bool:
)
valid = False

rule_names = [rule.name for rule in self._rules]

rule_inputs = self._get_direct_rule_inputs(rule_names)

# check for missing rule inputs
needed_rule_inputs = _lu.remove_duplicates_from_list(
self._get_direct_rule_inputs()
)
rule_input_vars = input_vars + mapping_vars_created
missing_rule_inputs = _lu.items_not_in(needed_rule_inputs, rule_input_vars)
if len(missing_rule_inputs) > 0:
logger.log_error(
f"Missing the variables '{', '.join(missing_rule_inputs)}' that "
"are required by some rules."
)
valid = False
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())
missing_rule_inputs = _lu.items_not_in(needed_rule_inputs, rule_input_vars)
if len(missing_rule_inputs) > 0:
logger.log_error(
f"Missing the variables '{', '.join(missing_rule_inputs)}' that "
f"are required by '{rule_name}'."
)
valid = False

return valid

def _get_direct_rule_inputs(self) -> List[str]:
def _get_direct_rule_inputs(self, rule_names) -> Dict[str, List[str]]:
"""Gets the input variables directly needed by rules from
input datasets.

Returns:
List[str]:
Dict[str, List[str]]
"""
rule_input_vars = _lu.flatten_list(
[rule.input_variable_names for rule in self._rules]
)
rule_input_vars = [rule.input_variable_names for rule in self._rules]
rule_output_vars = [rule.output_variable_name for rule in self._rules]

return _lu.items_not_in(rule_input_vars, rule_output_vars)
needed_input_per_rule = {}
for index, inputs_per_rule in enumerate(rule_input_vars):
needed_input_per_rule[rule_names[index]] = _lu.items_not_in(
inputs_per_rule, rule_output_vars
)

return needed_input_per_rule
7 changes: 5 additions & 2 deletions decoimpact/business/entities/rule_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import numpy as _np
import xarray as _xr
import decoimpact.business.utils.dataset_utils as _du
import decoimpact.business.utils.list_utils as _lu

from decoimpact.business.entities.rules.i_array_based_rule import IArrayBasedRule
from decoimpact.business.entities.rules.i_cell_based_rule import ICellBasedRule
Expand Down Expand Up @@ -63,8 +65,9 @@ def initialize(self, logger: ILogger) -> bool:
"""
inputs: List[str] = []

inputs = [str(key) for key in self._input_dataset]

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
2 changes: 1 addition & 1 deletion decoimpact/business/entities/rules/axis_filter_rule.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 Stichting Deltares and D-EcoImpact contributors
# Copyright (C) 2022-2024 Stichting Deltares and D-EcoImpact contributors
# This program is free software distributed under the GNU
# Lesser General Public License version 2.1
# A copy of the GNU General Public License can be found at
Expand Down
108 changes: 108 additions & 0 deletions decoimpact/business/entities/rules/depth_average_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2024 Stichting Deltares
# This program is free software distributed under the
# GNU Affero General Public License version 3.0
# A copy of the GNU Affero General Public License can be found at
# https://github.com/Deltares/D-EcoImpact/blob/main/LICENSE.md
"""
Module for DepthAverageRule class

Classes:
DepthAverageRule
"""
from typing import Dict

import xarray as _xr

from decoimpact.business.entities.rules.i_multi_array_based_rule import (
IMultiArrayBasedRule,
)
from decoimpact.business.entities.rules.rule_base import RuleBase
from decoimpact.crosscutting.i_logger import ILogger
from decoimpact.crosscutting.delft3d_specific_data import (
INTERFACES_NAME,
BED_LEVEL_NAME,
WATER_LEVEL_NAME,
)


class DepthAverageRule(RuleBase, IMultiArrayBasedRule):
"""Implementation for the depth average rule"""

def execute(
self, value_arrays: Dict[str, _xr.DataArray], logger: ILogger
) -> _xr.DataArray:
"""Calculate depth average of assumed z-layers.

Args:
mKlapwijk marked this conversation as resolved.
Show resolved Hide resolved
value_array (DataArray): Values to multiply

Returns:
DataArray: Averaged values
"""

# The first DataArray in our value_arrays contains the values to be averaged
# but the name of the key is given by the user, and is unknown here, so
# just used the first value.
variables = next(iter(value_arrays.values()))

# depths interfaces = borders of the layers in terms of depth
depths_interfaces = value_arrays[INTERFACES_NAME]
water_level_values = value_arrays[WATER_LEVEL_NAME]
bed_level_values = value_arrays[BED_LEVEL_NAME]

# Get the dimension names for the interfaces and for the layers
dim_interfaces_name = list(depths_interfaces.dims)[0]
mKlapwijk marked this conversation as resolved.
Show resolved Hide resolved
interfaces_len = depths_interfaces[dim_interfaces_name].size

dim_layer_name = [
d for d in variables.dims if d not in water_level_values.dims
][0]
layer_len = variables[dim_layer_name].size

# interface dimension should always be one larger than layer dimension
# Otherwise give an error to the user
if interfaces_len != layer_len + 1:
logger.log_error(
f"The number of interfaces should be number of layers + 1. Number of"
f"interfaces = {interfaces_len}. Number of layers = {layer_len}."
)
return variables

# Deal with open layer system at water level and bed level
depths_interfaces.values[depths_interfaces.values.argmin()] = -100000
depths_interfaces.values[depths_interfaces.values.argmax()] = 100000

# Broadcast the depths to the dimensions of the bed levels. Then make a
# correction for the depths to the bed level, in other words all depths lower
# than the bed level will be corrected to the bed level.
depths_interfaces_broadcasted = depths_interfaces.broadcast_like(
bed_level_values
)

corrected_depth_bed = depths_interfaces_broadcasted.where(
bed_level_values < depths_interfaces_broadcasted, bed_level_values
)

# Make a similar correction for the waterlevels (first broadcast to match
# dimensions and then replace all values higher than waterlevel with
# waterlevel)
corrected_depth_bed = corrected_depth_bed.broadcast_like(water_level_values)
corrected_depth_bed = corrected_depth_bed.where(
water_level_values > corrected_depth_bed, water_level_values
mKlapwijk marked this conversation as resolved.
Show resolved Hide resolved
)

# Calculate the layer heights between depths
layer_heights = corrected_depth_bed.diff(dim=dim_interfaces_name)
layer_heights = layer_heights.rename({dim_interfaces_name: dim_layer_name})

# Use the NaN filtering of the variables to set the correct depth per column
layer_heights = layer_heights.where(variables.notnull())

# Calculate depth average using relative value
relative_values = variables * layer_heights

# Calculate average
return relative_values.sum(dim=dim_layer_name) / layer_heights.sum(
dim=dim_layer_name
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 Stichting Deltares and D-EcoImpact contributors
# Copyright (C) 2022-2024 Stichting Deltares and D-EcoImpact contributors
# This program is free software distributed under the GNU
# Lesser General Public License version 2.1
# A copy of the GNU General Public License can be found at
Expand Down
14 changes: 13 additions & 1 deletion decoimpact/business/utils/dataset_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ def list_vars(dataset: _xr.Dataset) -> list[str]:
return list((dataset.data_vars or {}).keys())


def list_coords(dataset: _xr.Dataset) -> list[str]:
"""List coordinates in dataset

Args:
dataset (_xr.Dataset): Dataset to list variables from

Returns:
list_variables
"""
return list((dataset.coords or {}).keys())


def copy_dataset(dataset: _xr.Dataset) -> _xr.Dataset:
"""Copy dataset to new dataset

Expand Down Expand Up @@ -211,7 +223,7 @@ def get_dummy_variable_in_ugrid(dataset: _xr.Dataset) -> list:

def get_dummy_and_dependent_var_list(dataset: _xr.Dataset) -> list:
"""Obtain the list of variables in a dataset.
The dummy variable is obtained, from which a the variables are
The dummy variable is obtained, from which the variables are
recursively looked up. The dummy and dependent variables are combined
in one list.
This is done to support XUgrid and to prevent invalid topologies.
Expand Down
12 changes: 11 additions & 1 deletion decoimpact/business/workflow/model_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from decoimpact.business.entities.rules.axis_filter_rule import AxisFilterRule
from decoimpact.business.entities.rules.classification_rule import ClassificationRule
from decoimpact.business.entities.rules.combine_results_rule import CombineResultsRule
from decoimpact.business.entities.rules.depth_average_rule import DepthAverageRule
from decoimpact.business.entities.rules.formula_rule import FormulaRule
from decoimpact.business.entities.rules.i_rule import IRule
from decoimpact.business.entities.rules.layer_filter_rule import LayerFilterRule
Expand All @@ -39,6 +40,7 @@
from decoimpact.data.api.i_classification_rule_data import IClassificationRuleData
from decoimpact.data.api.i_combine_results_rule_data import ICombineResultsRuleData
from decoimpact.data.api.i_data_access_layer import IDataAccessLayer
from decoimpact.data.api.i_depth_average_rule_data import IDepthAverageRuleData
from decoimpact.data.api.i_formula_rule_data import IFormulaRuleData
from decoimpact.data.api.i_layer_filter_rule_data import ILayerFilterRuleData
from decoimpact.data.api.i_model_data import IModelData
Expand Down Expand Up @@ -90,13 +92,22 @@ def _set_default_fields(rule_data: IRuleData, rule: RuleBase):

@staticmethod
def _create_rule(rule_data: IRuleData) -> IRule:

# from python >3.10 we can use match/case, better solution
# until then disable pylint.
# pylint: disable=too-many-branches
mKlapwijk marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(rule_data, IMultiplyRuleData):
rule = MultiplyRule(
rule_data.name,
[rule_data.input_variable],
rule_data.multipliers,
rule_data.date_range,
)
elif isinstance(rule_data, IDepthAverageRuleData):
rule = DepthAverageRule(
rule_data.name,
rule_data.input_variables,
)
elif isinstance(rule_data, ILayerFilterRuleData):
rule = LayerFilterRule(
rule_data.name,
Expand Down Expand Up @@ -162,5 +173,4 @@ def _create_rule(rule_data: IRuleData) -> IRule:

if isinstance(rule, RuleBase):
ModelBuilder._set_default_fields(rule_data, rule)

return rule
13 changes: 13 additions & 0 deletions decoimpact/crosscutting/delft3d_specific_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2024 Stichting Deltares
# This program is free software distributed under the
# GNU Affero General Public License version 3.0
# A copy of the GNU Affero General Public License can be found at
# https://github.com/Deltares/D-EcoImpact/blob/main/LICENSE.md
"""
Configuration file for hardcoded delft3d variable names
"""

INTERFACES_NAME = "mesh2d_interface_z"
BED_LEVEL_NAME = "mesh2d_flowelem_bl"
WATER_LEVEL_NAME = "mesh2d_s1"
28 changes: 28 additions & 0 deletions decoimpact/data/api/i_depth_average_rule_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2024 Stichting Deltares
# This program is free software distributed under the
# GNU Affero General Public License version 3.0
# A copy of the GNU Affero General Public License can be found at
# https://github.com/Deltares/D-EcoImpact/blob/main/LICENSE.md
"""
Module for IDepthAverageRuleData interface

Interfaces:
IDepthAverageRuleData

"""


from abc import ABC, abstractmethod
from typing import List

from decoimpact.data.api.i_rule_data import IRuleData


class IDepthAverageRuleData(IRuleData, ABC):
"""Data for a DepthAverageRule"""

@property
@abstractmethod
def input_variables(self) -> List[str]:
"""List with input variable name and standard depth name"""
2 changes: 1 addition & 1 deletion decoimpact/data/api/i_rolling_statistics_rule_data.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 Stichting Deltares and D-EcoImpact contributors
# Copyright (C) 2022-2024 Stichting Deltares and D-EcoImpact contributors
# This program is free software distributed under the GNU
# Lesser General Public License version 2.1
# A copy of the GNU General Public License can be found at
Expand Down
Loading
Loading