Skip to content

Commit

Permalink
feat: Support Fortran scientific notation in .tim files (e.g., 1d0)
Browse files Browse the repository at this point in the history
Refs: #719
  • Loading branch information
arthurvd authored Dec 17, 2024
1 parent 1ef664e commit 93573a3
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 16 deletions.
18 changes: 2 additions & 16 deletions hydrolib/core/dflowfm/ini/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from abc import ABC
from enum import Enum
from math import isnan
from re import compile
from typing import (
Any,
Callable,
Expand All @@ -29,6 +28,7 @@
ParsableFileModel,
)

from ...utils import FortranUtils
from ..ini.io_models import CommentBlock, Document, Property, Section
from .parser import Parser
from .serializer import (
Expand Down Expand Up @@ -58,9 +58,6 @@ class INIBasedModel(BaseModel, ABC):

_header: str = ""
_file_path_style_converter = FilePathStyleConverter()
_scientific_notation_regex = compile(
r"([\d.]+)([dD])([+-]?\d{1,3})"
) # matches a float: 1d9, 1D-3, 1.D+4, etc.

class Config:
extra = Extra.ignore
Expand Down Expand Up @@ -159,18 +156,7 @@ def replace_fortran_scientific_notation_for_floats(cls, value, field):
if field.type_ != float:
return value

return cls._replace_fortran_scientific_notation(value)

@classmethod
def _replace_fortran_scientific_notation(cls, value):
if isinstance(value, str):
return cls._scientific_notation_regex.sub(r"\1e\3", value)
if isinstance(value, list):
for i, v in enumerate(value):
if isinstance(v, str):
value[i] = cls._scientific_notation_regex.sub(r"\1e\3", v)

return value
return FortranUtils.replace_fortran_scientific_notation(value)

@classmethod
def validate(cls: Type["INIBasedModel"], value: Any) -> "INIBasedModel":
Expand Down
21 changes: 21 additions & 0 deletions hydrolib/core/dflowfm/tim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic.v1.class_validators import validator

from hydrolib.core.basemodel import BaseModel, ModelSaveSettings, ParsableFileModel
from hydrolib.core.utils import FortranUtils

from .parser import TimParser
from .serializer import TimSerializer, TimSerializerConfig
Expand Down Expand Up @@ -49,6 +50,26 @@ def _get_serializer(
def _get_parser(cls) -> Callable[[Path], Dict]:
return TimParser.parse

@validator("timeseries", pre=True, check_fields=True, allow_reuse=True)
def replace_fortran_scientific_notation_for_floats(cls, value, field):
for record in value:
if isinstance(record, dict):
record["time"] = FortranUtils.replace_fortran_scientific_notation(
record["time"]
)
record["data"] = FortranUtils.replace_fortran_scientific_notation(
record["data"]
)
elif isinstance(record, TimRecord):
record.time = FortranUtils.replace_fortran_scientific_notation(
record.time
)
record.data = FortranUtils.replace_fortran_scientific_notation(
record.data
)

return value

@validator("timeseries")
@classmethod
def _validate_timeseries_values(cls, v: List[TimRecord]) -> List[TimRecord]:
Expand Down
22 changes: 22 additions & 0 deletions hydrolib/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from pathlib import Path
from typing import Any, Callable, List, Optional

from pydantic import validator
from pydantic.v1.fields import ModelField
from strenum import StrEnum


Expand Down Expand Up @@ -332,3 +334,23 @@ def _calculate_md5_checksum(filepath: Path) -> str:
for chunk in iter(lambda: file.read(4096), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()


class FortranUtils:
"""Utility class for Fortran specific conventions."""

_scientific_exp_d_notation_regex = re.compile(
r"([\d.]+)([dD])([+-]?\d{1,3})"
) # matches a float: 1d9, 1D-3, 1.D+4, etc.

@staticmethod
def replace_fortran_scientific_notation(value):
"""Replace Fortran scientific notation ("D" in exponent) with standard
scientific notation ("e" in exponent).
"""
if isinstance(value, str):
return FortranUtils._scientific_exp_d_notation_regex.sub(r"\1e\3", value)
elif isinstance(value, list):
return list(map(FortranUtils.replace_fortran_scientific_notation, value))

return value
2 changes: 2 additions & 0 deletions tests/data/input/tim/unimagdir.wnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
0.0 1.00 270.00
9d9 1.00 270.00
9 changes: 9 additions & 0 deletions tests/dflowfm/test_tim.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import math
from pathlib import Path

import pytest
Expand Down Expand Up @@ -223,6 +224,14 @@ def test_validate_data_for_timeseries_throws_exception_for_incorrect_data(

assert expected_error_msg in str(error.value)

def test_test_fortran_d_exponent_supported(self):
input_path = Path(test_input_dir / "tim" / "unimagdir.wnd")
tim_model = TimModel(input_path)
assert tim_model.timeseries[0].time == 0
assert tim_model.timeseries[0].data == [1.0, 270.0]
assert math.isclose(tim_model.timeseries[1].time, 9e9)
assert tim_model.timeseries[1].data == [1.0, 270.0]


class TestTimRecord:
def test_initialization(self):
Expand Down

0 comments on commit 93573a3

Please sign in to comment.