Skip to content

Commit

Permalink
Merge pull request #88 from Deltares/feature/DEI-141-layer-filter-rul…
Browse files Browse the repository at this point in the history
…e-on-other-axis

Feature/dei  axis filter rule
  • Loading branch information
IoannaMi authored Nov 22, 2023
2 parents 9a2015b + 7361c7f commit 886a4af
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 0 deletions.
77 changes: 77 additions & 0 deletions decoimpact/business/entities/rules/axis_filter_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 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
# https://github.com/Deltares/D-EcoImpact/blob/main/LICENSE.md
"""
Module for AxisFilterRule class
Classes:
AxisFilterRule
"""

from typing import List

import xarray as _xr

from decoimpact.business.entities.rules.i_array_based_rule import IArrayBasedRule
from decoimpact.business.entities.rules.rule_base import RuleBase
from decoimpact.crosscutting.i_logger import ILogger


class AxisFilterRule(RuleBase, IArrayBasedRule):

"""Implementation for the layer filter rule"""

def __init__(
self,
name: str,
input_variable_names: List[str],
layer_number: int,
axis_name: str,
output_variable_name: str = "output",
description: str = "",
):
super().__init__(name, input_variable_names, output_variable_name, description)
self._layer_number = layer_number
self._axis_name = axis_name

@property
def layer_number(self) -> int:
"""Layer number property"""
return self._layer_number

@property
def axis_name(self) -> str:
"""Layer number property"""
return self._axis_name

def execute(self, value_array: _xr.DataArray, logger: ILogger) -> _xr.DataArray:

"""Obtain a 2D layer from a 3D variable
Args:
value (float): 3D value to obtain a layer from
Returns:
float: 2D variable
"""

if self._axis_name not in value_array.dims:
message = f"""Layer name is not in dim names \
[{value_array.dims}] layer_name [{self._axis_name}]"""
logger.log_error(message)
raise IndexError(message)

if not (
self._layer_number >= 0
and self._layer_number <= len(getattr(value_array, self._axis_name))
):
message = f"""Layer number should be within range \
[0,{len(getattr(value_array, self._axis_name))}]"""
logger.log_error(message)
raise IndexError(message)

return value_array.isel({self._axis_name: self._layer_number - 1})
11 changes: 11 additions & 0 deletions decoimpact/business/workflow/model_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from decoimpact.business.entities.i_model import IModel
from decoimpact.business.entities.rule_based_model import RuleBasedModel
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.formula_rule import FormulaRule
Expand All @@ -33,6 +34,7 @@
from decoimpact.business.entities.rules.time_aggregation_rule import TimeAggregationRule
from decoimpact.business.workflow.i_model_builder import IModelBuilder
from decoimpact.crosscutting.i_logger import ILogger
from decoimpact.data.api.i_axis_filter_rule_data import IAxisFilterRuleData
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
Expand Down Expand Up @@ -98,6 +100,15 @@ def _create_rule(rule_data: IRuleData) -> IRule:
rule_data.layer_number,
rule_data.output_variable,
)

if isinstance(rule_data, IAxisFilterRuleData):
return AxisFilterRule(
rule_data.name,
[rule_data.input_variable],
rule_data.layer_number,
rule_data.axis_name,
rule_data.output_variable,
)

if isinstance(rule_data, IStepFunctionRuleData):
return StepFunctionRule(
Expand Down
37 changes: 37 additions & 0 deletions decoimpact/data/api/i_axis_filter_rule_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 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 IAxisFilterRuleData interface
Interfaces:
IAxisFilterRuleData
"""


from abc import ABC, abstractmethod

from decoimpact.data.api.i_rule_data import IRuleData


class IAxisFilterRuleData(IRuleData, ABC):
"""Data for a axis filter rule"""

@property
@abstractmethod
def input_variable(self) -> str:
"""Property for the nput variable"""

@property
@abstractmethod
def layer_number(self) -> int:
"""Property for the layer number"""

@property
@abstractmethod
def axis_name(self) -> str:
"""Property for the dim name"""
50 changes: 50 additions & 0 deletions decoimpact/data/entities/axis_filter_rule_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 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 AxisFilterRuleData class
Classes:
AxisFilterRuleData
"""

from decoimpact.data.api.i_axis_filter_rule_data import IAxisFilterRuleData
from decoimpact.data.entities.rule_data import RuleData


class AxisFilterRuleData(IAxisFilterRuleData, RuleData):
"""Class for storing data related to axis filter rule rule"""

def __init__(
self,
name: str,
layer_number: int,
axis_name: str,
input_variable: str,
output_variable: str = "output",
description: str = "",
):
super().__init__(name, output_variable, description)
self._input_variable = input_variable
self._layer_number = layer_number
self._axis_name = axis_name

@property
def input_variable(self) -> str:
"""Property for the input variable"""
return self._input_variable

@property
def layer_number(self) -> int:
"""Property for the layer number"""
return self._layer_number

@property
def axis_name(self) -> str:
"""Property for the dimension name"""
return self._axis_name

62 changes: 62 additions & 0 deletions decoimpact/data/parsers/parser_axis_filter_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 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 ParserLayerFilterRule class
Classes:
ParserLayerFilterRule
"""
from typing import Any, Dict

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.axis_filter_rule_data import AxisFilterRuleData
from decoimpact.data.parsers.i_parser_rule_base import IParserRuleBase


class ParserAxisFilterRule(IParserRuleBase):

"""Class for creating a AxisFilterRuleData"""

@property
def rule_type_name(self) -> str:
"""Type name for the rule"""
return "axis_filter_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 = get_dict_element("name", dictionary)
description = get_dict_element("description", dictionary)
input_variable_name = get_dict_element("input_variable", dictionary)
axis_name = get_dict_element("axis_name", dictionary)
if not isinstance(axis_name, str):
message = (
"Dimension name should be a string, "
f"received a {type(axis_name)}: {axis_name}"
)
raise ValueError(message)

layer_number = get_dict_element("layer_number", dictionary)
if not isinstance(layer_number, int):
message = (
"Layer number should be an integer, "
f"received a {type(layer_number)}: {layer_number}"
)
raise ValueError(message)
output_variable_name = get_dict_element("output_variable", dictionary)

return AxisFilterRuleData(
name, layer_number, axis_name, input_variable_name,
output_variable_name, description
)
2 changes: 2 additions & 0 deletions decoimpact/data/parsers/rule_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import Iterator

from decoimpact.data.parsers.i_parser_rule_base import IParserRuleBase
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_formula_rule import ParserFormulaRule
Expand All @@ -39,3 +40,4 @@ def rule_parsers() -> Iterator[IParserRuleBase]:
yield ParserResponseCurveRule()
yield ParserFormulaRule()
yield ParserClassificationRule()
yield ParserAxisFilterRule()
47 changes: 47 additions & 0 deletions tests/business/entities/rules/test_axis_filter_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This file is part of D-EcoImpact
# Copyright (C) 2022-2023 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
"""
Tests for AxisFilterRule class
"""
import pytest
import xarray as _xr
from mock import Mock

from decoimpact.business.entities.rules.axis_filter_rule import AxisFilterRule
from decoimpact.crosscutting.i_logger import ILogger


def test_create_axis_filter_rule_should_set_defaults():
"""Test creating a AxisFilterRule with defaults"""

# Arrange & Act
rule = AxisFilterRule("test", ["foo"], 3, "boo", "output")

# Assert
assert rule.name == "test"
assert rule.description == ""
assert rule.input_variable_names == ["foo"]
assert rule.output_variable_name == "output"
assert rule.layer_number == 3
assert rule.axis_name == "boo"
assert isinstance(rule, AxisFilterRule)


def test_execute_value_array_axis_filtered():
"""Test execute of layer filter rule"""
# Arrange & Act
logger = Mock(ILogger)
rule = AxisFilterRule("test", ["foo"], 1, "dim_1", "output", "description")
data = [[1, 2], [3, 4]]
value_array = _xr.DataArray(data, dims=("dim_1","dim_2"))
filtered_array = rule.execute(value_array, logger)

result_data = [1,2]
result_array = _xr.DataArray(result_data, dims=("dim_2"))

# Assert
assert _xr.testing.assert_equal(filtered_array, result_array) is None
28 changes: 28 additions & 0 deletions tests/data/entities/test_axis_filter_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-2023 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
"""
Tests for AxisFilterRuleData class
"""

from decoimpact.data.api.i_rule_data import IRuleData
from decoimpact.data.entities.axis_filter_rule_data import AxisFilterRuleData


def test_axis_filter_rule_data_creation_logic():
"""The AxisFilterRuleData should parse the provided dictionary
to correctly initialize itself during creation """

#Act
data = AxisFilterRuleData("test_name", 3, "axis_name", "input", "output", "description")


#Assert

assert isinstance(data, IRuleData)
assert data.input_variable == "input"
assert data.layer_number == 3
assert data.axis_name == "axis_name"
Loading

0 comments on commit 886a4af

Please sign in to comment.