Skip to content

Commit

Permalink
feat: Add validation for NaN values in datablocks
Browse files Browse the repository at this point in the history
Refs: #433
  • Loading branch information
tim-vd-aardweg authored Mar 17, 2023
1 parent b849104 commit c322da7
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 5 deletions.
31 changes: 28 additions & 3 deletions hydrolib/core/dflowfm/ini/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from abc import ABC
from enum import Enum
from math import isnan
from typing import Any, Callable, List, Literal, Optional, Set, Type, Union

from pydantic import Extra, Field, root_validator
Expand Down Expand Up @@ -167,15 +168,17 @@ def _to_section(self, config: INISerializerConfig) -> Section:
return Section(header=self._header, content=props)


Datablock = List[List[Union[float, str]]]


class DataBlockINIBasedModel(INIBasedModel):
"""DataBlockINIBasedModel defines the base model for ini models with datablocks.
Attributes:
datablock (List[List[Union[float, str]]]): (class attribute) the actual data
columns.
datablock (Datablock): (class attribute) the actual data columns.
"""

datablock: List[List[Union[float, str]]] = []
datablock: Datablock = []

_make_lists = make_list_validator("datablock")

Expand Down Expand Up @@ -204,6 +207,28 @@ def convert_value(

return value

@validator("datablock")
def _validate_no_nans_are_present(cls, datablock: Datablock) -> Datablock:
"""Validate that the datablock does not have any NaN values.
Args:
datablock (Datablock): The datablock to verify.
Raises:
ValueError: When a NaN is present in the datablock.
Returns:
Datablock: The validated datablock.
"""
if any(cls._is_float_and_nan(value) for list in datablock for value in list):
raise ValueError("NaN is not supported in datablocks.")

return datablock

@staticmethod
def _is_float_and_nan(value: float) -> bool:
return isinstance(value, float) and isnan(value)


class INIGeneral(INIBasedModel):
_header: Literal["General"] = "General"
Expand Down
41 changes: 41 additions & 0 deletions tests/dflowfm/ini/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from math import nan

import pytest
from pydantic.error_wrappers import ValidationError

from hydrolib.core.dflowfm.ini.models import DataBlockINIBasedModel

from ...utils import error_occurs_only_once


class TestDataBlockINIBasedModel:
def test_datablock_with_nan_values_should_raise_error(self):
model = DataBlockINIBasedModel()

with pytest.raises(ValidationError) as error:
model.datablock = [[nan, 0], [-1, 2]]

expected_message = "NaN is not supported in datablocks."
assert expected_message in str(error.value)

def test_updating_datablock_with_nan_values_should_raise_error(self):
model = DataBlockINIBasedModel()

valid_datablock = [[0, 1], [2, 3]]
model.datablock = valid_datablock

invalid_datablock = [[nan, 1], [2, 3]]
with pytest.raises(ValidationError) as error:
model.datablock = invalid_datablock

expected_message = "NaN is not supported in datablocks."
assert expected_message in str(error.value)

def test_datablock_with_multiple_nans_should_only_give_error_once(self):
model = DataBlockINIBasedModel()

with pytest.raises(ValidationError) as error:
model.datablock = [[nan, nan], [nan, nan]]

expected_message = "NaN is not supported in datablocks."
assert error_occurs_only_once(expected_message, str(error.value))
21 changes: 21 additions & 0 deletions tests/dflowfm/test_bc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path
from typing import Dict, List, Literal

import numpy as np
import pytest
from pydantic.error_wrappers import ValidationError

Expand Down Expand Up @@ -357,6 +358,26 @@ def test_forcing_model_correct_default_serializer_config(self):
assert model.serializer_config.comment_delimiter == "#"
assert model.serializer_config.skip_empty_properties == True

def test_forcing_model_with_datablock_that_has_nan_values_should_raise_error(self):
datablock = np.random.uniform(low=-40, high=130.3, size=(4, 2)) * np.nan
datablock_list = datablock.tolist()

with pytest.raises(ValidationError) as error:
TimeSeries(
name="east2_0001",
quantityunitpair=[
QuantityUnitPair(
quantity="time", unit="seconds since 2022-01-01 00:00:00 +00:00"
),
QuantityUnitPair(quantity="waterlevel", unit="m"),
],
timeInterpolation=TimeInterpolation.linear,
datablock=datablock_list,
)

expected_message = "NaN is not supported in datablocks."
assert expected_message in str(error.value)


class TestVectorForcingBase:
class VectorForcingTest(VectorForcingBase):
Expand Down
19 changes: 17 additions & 2 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
from pathlib import Path
from typing import Generic, Optional, TypeVar

import pytest
from numpy import array
from pydantic.generics import GenericModel

TWrapper = TypeVar("TWrapper")
Expand Down Expand Up @@ -89,3 +87,20 @@ def assert_file_is_same_binary(
assert filecmp.cmp(input_path, reference_path)
else:
assert not input_path.exists()


def error_occurs_only_once(error_message: str, full_error: str) -> bool:
"""Check if the given error message occurs exactly once in the full error string.
Args:
error_message (str): The error to check for.
full_error (str): The full error as a string.
Returns:
bool: Return True if the error message occurs exactly once in the full error.
Returns False otherwise.
"""
if error_message is None or full_error is None:
return False

return full_error.count(error_message) == 1

0 comments on commit c322da7

Please sign in to comment.