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 225 filter extremes rule #122

Merged
merged 28 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
810d229
add first setup filter extremes rule
CindyvdVries Jul 30, 2024
bf63b2c
Merge branch 'main' into feature/DEI-225-filter-extremes-rule
CindyvdVries Jul 30, 2024
64b5e13
test on how to implement find_peaks method
CindyvdVries Jul 30, 2024
34cd5a2
first working prototype filter extremes rule
CindyvdVries Aug 1, 2024
57aaaf1
update working version filter extremes with distance
CindyvdVries Aug 1, 2024
dde66d7
use distance and mask from user input
CindyvdVries Aug 2, 2024
def238b
implement timeoperationsettings
CindyvdVries Aug 2, 2024
ac16137
WIP set up test
CindyvdVries Aug 5, 2024
1d02401
first Test
CindyvdVries Aug 5, 2024
a3d40a3
adding tests for extreme filter rule
CindyvdVries Aug 5, 2024
1c01c87
add tests for parser and data object
CindyvdVries Aug 5, 2024
7059c7c
update documentation
CindyvdVries Aug 5, 2024
520fdee
Merge branch 'main' into feature/DEI-225-filter-extremes-rule
CindyvdVries Aug 5, 2024
a5f702d
acceptance test, mkdocs and clean up
CindyvdVries Aug 6, 2024
1552018
update documentation
CindyvdVries Aug 6, 2024
c0f0921
pylint
CindyvdVries Aug 6, 2024
61ce875
update review part 1
CindyvdVries Aug 6, 2024
4607b6d
Merge branch 'main' into feature/DEI-225-filter-extremes-rule
CindyvdVries Aug 6, 2024
cea14d0
update review part 2
CindyvdVries Aug 6, 2024
59fe32d
clarify documentation to include local maxima and local minima
mKlapwijk Aug 7, 2024
e3690a0
change sentence for readability
mKlapwijk Aug 7, 2024
c520047
add check on extreme_type options
CindyvdVries Aug 8, 2024
f30af19
Merge branch 'main' into feature/DEI-225-filter-extremes-rule
CindyvdVries Aug 8, 2024
52f3721
import mistake
CindyvdVries Aug 8, 2024
8121c43
update test
CindyvdVries Aug 8, 2024
c918386
Update parser_filter_extremes_rule.py
CindyvdVries Aug 9, 2024
e395267
fix for lower python versions (<3.11)
CindyvdVries Aug 9, 2024
bb6ba00
and get back the str not the enumerate
CindyvdVries Aug 9, 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
15 changes: 8 additions & 7 deletions decoimpact/business/entities/rules/depth_average_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ def execute(
variables = next(iter(value_arrays.values()))

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

# Get the dimension names for the interfaces and for the layers
dim_interfaces_name = list(depths_interfaces.dims)[0]
Expand Down Expand Up @@ -110,10 +113,8 @@ def execute(
)

def _extract_variable_based_on_suffix(
self,
value_arrays: Dict[str, _xr.DataArray],
suffix: str
) -> List:
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.

Expand Down
128 changes: 128 additions & 0 deletions decoimpact/business/entities/rules/filter_extremes_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# 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 FilterExtremesRule class

Classes:
FilterExtremesRule
"""

from typing import List

import xarray as _xr
import scipy as _sc
import numpy as _np

from decoimpact.business.entities.rules.i_array_based_rule import IArrayBasedRule
from decoimpact.business.entities.rules.rule_base import RuleBase
from decoimpact.business.entities.rules.time_operation_settings import (
TimeOperationSettings,
)
from decoimpact.business.utils.data_array_utils import get_time_dimension_name
from decoimpact.crosscutting.i_logger import ILogger
from decoimpact.data.dictionary_utils import get_dict_element


class FilterExtremesRule(RuleBase, IArrayBasedRule):
"""Implementation for the filter extremes rule"""

def __init__(
self,
name: str,
input_variable_names: List[str],
extreme_type: str,
distance: int,
time_scale: str,
mask: bool,
):
super().__init__(name, input_variable_names)
self._settings = TimeOperationSettings(
{"second": "s", "hour": "h", "day": "D", "month": "M", "year": "Y"}
)
self._extreme_type = extreme_type
self._distance = distance
self._settings.time_scale = time_scale
self._mask = mask

@property
def settings(self):
"""Time operation settings"""
return self._settings

@property
def extreme_type(self) -> str:
"""Type of extremes (peaks or troughs)"""
return self._extreme_type

@property
def distance(self) -> int:
"""Minimal distance between peaks"""
return self._distance

@property
def mask(self) -> bool:
"""Return either directly the values of the filtered array or a
True/False array"""
return self._mask

def validate(self, logger: ILogger) -> bool:
"""Validates if the rule is valid

Returns:
bool: wether the rule is valid
"""
return self.settings.validate(self.name, logger)

def execute(self, value_array: _xr.DataArray, logger: ILogger) -> _xr.DataArray:
"""Retrieve the peak or through values
Args:
value_array (DataArray): Values to filter at extremes
Returns:
DataArray: Filtered DataArray with only the extremes remaining
at all other times the values are set to NaN
"""

time_scale = get_dict_element(
self.settings.time_scale, self.settings.time_scale_mapping
)

time_dim_name = get_time_dimension_name(value_array, logger)
old_dr = _xr.DataArray(value_array)
time = old_dr.time.values
timestep = (time[-1] - time[0]) / len(time)
width_time = _np.timedelta64(self.distance, time_scale)
distance = width_time / timestep

results = _xr.apply_ufunc(
self._process_peaks,
old_dr,
input_core_dims=[[time_dim_name]],
output_core_dims=[[time_dim_name]],
vectorize=True,
kwargs={
"distance": distance,
"mask": self.mask,
"extreme_type": self.extreme_type,
},
)

results = results.transpose(*old_dr.dims)
return results

def _process_peaks(
self, arr: _xr.DataArray, distance: float, mask: bool, extreme_type: str
):
factor = 1
if extreme_type == "troughs":
factor = -1
peaks, _ = _sc.signal.find_peaks(factor * arr, distance=distance)
values = arr[peaks]
if mask:
values = True
new_arr = _np.full_like(arr, _np.nan, dtype=float)
new_arr[peaks] = values
return new_arr
11 changes: 11 additions & 0 deletions decoimpact/business/workflow/model_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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.filter_extremes_rule import FilterExtremesRule
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 @@ -41,6 +42,7 @@
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_filter_extremes_rule_data import IFilterExtremesRuleData
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 @@ -108,6 +110,15 @@ def _create_rule(rule_data: IRuleData) -> IRule:
rule_data.name,
rule_data.input_variables,
)
elif isinstance(rule_data, IFilterExtremesRuleData):
rule = FilterExtremesRule(
rule_data.name,
rule_data.input_variables,
rule_data.extreme_type,
rule_data.distance,
rule_data.time_scale,
rule_data.mask,
)
elif isinstance(rule_data, ILayerFilterRuleData):
rule = LayerFilterRule(
rule_data.name,
Expand Down
45 changes: 45 additions & 0 deletions decoimpact/data/api/i_filter_extremes_rule_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 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 IFilterExtremesRuleData interface

Interfaces:
IFilterExtremesRuleData
"""

from abc import ABC, abstractmethod
from typing import List

from decoimpact.data.api.i_rule_data import IRuleData


class IFilterExtremesRuleData(IRuleData, ABC):
"""Data for a filter extremes rule"""

@property
@abstractmethod
def input_variables(self) -> List[str]:
"""List with input variable name"""

@property
@abstractmethod
def extreme_type(self) -> str:
"""Type of extremes [peaks or throughs]"""

@property
@abstractmethod
def distance(self) -> int:
"""Property for the distance between peaks"""

@property
@abstractmethod
def time_scale(self) -> str:
"""Property for the timescale of the distance between peaks"""

@property
@abstractmethod
def mask(self) -> bool:
"""Property for mask"""
4 changes: 2 additions & 2 deletions decoimpact/data/entities/depth_average_rule_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# 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 (multiple) ClassificationRule class
Module for (multiple) DepthAverageRule class

Classes:
(multiple) ClassificationRuleData
(multiple) DepthAverageRuleData

"""

Expand Down
63 changes: 63 additions & 0 deletions decoimpact/data/entities/filter_extremes_rule_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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 FilterExtremesRuleData class

Classes:
FilterExtremesRuleData

"""

from typing import List

from decoimpact.data.api.i_filter_extremes_rule_data import IFilterExtremesRuleData
from decoimpact.data.entities.rule_data import RuleData


class FilterExtremesRuleData(IFilterExtremesRuleData, RuleData):
"""Class for storing data related to filter extremes rule"""

def __init__(
self,
name: str,
input_variables: List[str],
extreme_type: str,
distance: int,
time_scale: str,
mask: bool,
):
super().__init__(name)
self._input_variables = input_variables
self._extreme_type = extreme_type
self._distance = distance
self._time_scale = time_scale
self._mask = mask

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

@property
def extreme_type(self) -> str:
"""Property for the extremes type"""
return self._extreme_type

@property
def distance(self) -> int:
"""Property for the distance between peaks"""
return self._distance

@property
def time_scale(self) -> str:
"""Property for the timescale of the distance between peaks"""
return self._time_scale

@property
def mask(self) -> bool:
"""Property for mask"""
return self._mask
56 changes: 56 additions & 0 deletions decoimpact/data/parsers/parser_filter_extremes_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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 ParserFilterExtremesRule class

Classes:
ParserFilterExtremesRule
"""
from typing import Any, Dict, List

from decoimpact.crosscutting.i_logger import ILogger
from decoimpact.data.api.i_rule_data import IRuleData
from decoimpact.data.dictionary_utils import get_dict_element
from decoimpact.data.entities.filter_extremes_rule_data import FilterExtremesRuleData
from decoimpact.data.parsers.i_parser_rule_base import IParserRuleBase


class ParserFilterExtremesRule(IParserRuleBase):
"""Class for creating a ParserFilterExtremesRule"""

@property
def rule_type_name(self) -> str:
"""Type name for the rule"""
return "filter_extremes_rule"

def parse_dict(self, dictionary: Dict[str, Any], logger: ILogger) -> IRuleData:
"""Parses the provided dictionary to a IRuleData
Args:
dictionary (Dict[str, Any]): Dictionary holding the values
for making the rule
Returns:
RuleBase: Rule based on the provided data
"""
name: str = get_dict_element("name", dictionary)
input_variable_names: List[str] = [
get_dict_element("input_variable", dictionary)
]
output_variable_name: str = get_dict_element("output_variable", dictionary)
description: str = get_dict_element("description", dictionary, False) or ""
extreme_type: str = get_dict_element("extreme_type", dictionary)
distance: int = get_dict_element("distance", dictionary) or 0
time_scale: str = get_dict_element("time_scale", dictionary) or "D"

mask: bool = get_dict_element("mask", dictionary) or False
rule_data = FilterExtremesRuleData(
name, input_variable_names, extreme_type, distance, time_scale, mask
)

rule_data.output_variable = output_variable_name
rule_data.description = description

return rule_data
2 changes: 2 additions & 0 deletions decoimpact/data/parsers/rule_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from decoimpact.data.parsers.parser_axis_filter_rule import ParserAxisFilterRule
from decoimpact.data.parsers.parser_classification_rule import ParserClassificationRule
from decoimpact.data.parsers.parser_combine_results_rule import ParserCombineResultsRule
from decoimpact.data.parsers.parser_filter_extremes_rule import ParserFilterExtremesRule
from decoimpact.data.parsers.parser_formula_rule import ParserFormulaRule
from decoimpact.data.parsers.parser_layer_filter_rule import ParserLayerFilterRule
from decoimpact.data.parsers.parser_multiply_rule import ParserMultiplyRule
Expand Down Expand Up @@ -43,3 +44,4 @@ def rule_parsers() -> Iterator[IParserRuleBase]:
yield ParserClassificationRule()
yield ParserAxisFilterRule()
yield ParserDepthAverageRule()
yield ParserFilterExtremesRule()
Binary file added docs/assets/images/3_result_filter_extremes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading