Skip to content

Commit

Permalink
feat: initial condition converter from old external forcing file
Browse files Browse the repository at this point in the history
feat: initial condition converter from old external forcing file
  • Loading branch information
MAfarrag authored Dec 12, 2024
1 parent 2fb8c8b commit 037cbc3
Show file tree
Hide file tree
Showing 29 changed files with 2,977 additions and 1,131 deletions.
1 change: 0 additions & 1 deletion .flake8

This file was deleted.

8 changes: 6 additions & 2 deletions hydrolib/core/basemodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,9 @@ class ModelSaveSettings:

_os_path_style = get_path_style_for_current_operating_system()

def __init__(self, path_style: Optional[PathStyle] = None, exclude_unset: bool = False) -> None:
def __init__(
self, path_style: Optional[PathStyle] = None, exclude_unset: bool = False
) -> None:
"""Initializes a new instance of the ModelSaveSettings class.
Args:
Expand Down Expand Up @@ -1054,7 +1056,9 @@ def save(
self.filepath = filepath

path_style = path_style_validator.validate(path_style)
save_settings = ModelSaveSettings(path_style=path_style, exclude_unset=exclude_unset)
save_settings = ModelSaveSettings(
path_style=path_style, exclude_unset=exclude_unset
)

# Handle save
with file_load_context() as context:
Expand Down
23 changes: 9 additions & 14 deletions hydrolib/core/dflowfm/ext/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@
)
from hydrolib.core.dflowfm.bc.models import ForcingBase, ForcingData, ForcingModel
from hydrolib.core.dflowfm.common.models import Operand
from hydrolib.core.dflowfm.ini.models import (
INIBasedModel,
INIGeneral,
INIModel,
INISerializerConfig,
)
from hydrolib.core.dflowfm.ini.models import INIBasedModel, INIGeneral, INIModel
from hydrolib.core.dflowfm.ini.serializer import INISerializerConfig
from hydrolib.core.dflowfm.ini.util import (
LocationValidationConfiguration,
Expand Down Expand Up @@ -102,7 +97,7 @@ def _get_identifier(self, data: dict) -> Optional[str]:
return data.get("nodeid")

@property
def forcing(self) -> ForcingBase:
def forcing(self) -> Union[ForcingBase, None]:
"""Retrieves the corresponding forcing data for this boundary.
Returns:
Expand Down Expand Up @@ -214,7 +209,7 @@ class MeteoForcingFileType(StrEnum):
netcdf = "netcdf"
"""str: NetCDF, either with gridded data, or multiple station time series."""

allowedvaluestext = "Possible values: bcAscii, netcdf, uniform."
allowedvaluestext = "Possible values: bcAscii, uniform, uniMagDir, meteoGridEqui, spiderweb, meteoGridCurvi, netcdf."


class MeteoInterpolationMethod(StrEnum):
Expand All @@ -224,10 +219,10 @@ class MeteoInterpolationMethod(StrEnum):
"""

nearestnb = "nearestNb"
linearSpaceTime = "linearSpaceTime"
"""str: Nearest-neighbour interpolation, only with station-data in forcingFileType=netcdf"""

allowedvaluestext = "Possible values: nearestNb (only with station data in forcingFileType=netcdf ). "
linearSpaceTime = "linearSpaceTime"
"""str: Linear interpolation in space and time."""
allowedvaluestext = "Possible values: nearestNb, linearSpaceTime."


class Meteo(INIBasedModel):
Expand Down Expand Up @@ -345,9 +340,9 @@ class ExtModel(INIModel):
"""

general: ExtGeneral = ExtGeneral()
boundary: List[Boundary] = []
lateral: List[Lateral] = []
meteo: List[Meteo] = []
boundary: List[Boundary] = Field(default_factory=list)
lateral: List[Lateral] = Field(default_factory=list)
meteo: List[Meteo] = Field(default_factory=list)
serializer_config: INISerializerConfig = INISerializerConfig(
section_indent=0, property_indent=0
)
Expand Down
78 changes: 71 additions & 7 deletions hydrolib/core/dflowfm/extold/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
from hydrolib.core.dflowfm.polyfile.models import PolyFile
from hydrolib.core.dflowfm.tim.models import TimModel

INITIAL_CONDITION_QUANTITIES_VALID_PREFIXES = (
"initialtracer",
"initialsedfrac",
"initialverticalsedfracprofile",
"initialverticalsigmasedfracprofile",
)


HEADER = """
QUANTITY : waterlevelbnd, velocitybnd, dischargebnd, tangentialvelocitybnd, normalvelocitybnd filetype=9 method=2,3
: outflowbnd, neumannbnd, qhbnd, uxuyadvectionvelocitybnd filetype=9 method=2,3
Expand Down Expand Up @@ -151,7 +159,7 @@ class ExtOldBoundaryQuantity(StrEnum):
NormalVelocityBnd = "normalvelocitybnd"
"""Normal velocity"""
TangentialVelocityBnd = "tangentialvelocitybnd"
"""Tangentional velocity"""
"""Tangential velocity"""
QhBnd = "qhbnd"
"""Discharge-water level dependency"""

Expand Down Expand Up @@ -223,6 +231,62 @@ class ExtOldMeteoQuantity(StrEnum):
"""Dewpoint temperature"""


class ExtOldInitialConditionQuantity(StrEnum):
"""
Initial Condition quantities:
initialwaterlevel, initialsalinity, initialsalinitytop, initialtemperature,
initialverticaltemperatureprofile, initialverticalsalinityprofile, initialvelocityx,
initialvelocityy, initialvelocity
If there is a missing quantity that is mentioned in the "Accepted quantity names" section of the user manual
[Sec.C.5.3](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.C.5.3).
and [Sec.D.3](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.D.3).
please open and issue in github.
"""

# Initial Condition fields
BedLevel = "bedlevel"
BedLevel1D = "bedlevel1D"
BedLevel2D = "bedlevel2D"

InitialWaterLevel = "initialwaterlevel"
InitialWaterLevel1D = "initialwaterlevel1d"
InitialWaterLevel2D = "initialwaterlevel2d"

InitialSalinity = "initialsalinity"
InitialSalinityTop = "initialsalinitytop"
InitialSalinityBot = "initialsalinitybot"
InitialVerticalSalinityProfile = "initialverticalsalinityprofile"

InitialTemperature = "initialtemperature"
InitialVerticalTemperatureProfile = "initialverticaltemperatureprofile"

initialUnsaturatedZoneThickness = "initialunsaturatedzonethickness"
InitialVelocityX = "initialvelocityx"
InitialVelocityY = "initialvelocityy"
InitialVelocity = "initialvelocity"
InitialWaqBot = "initialwaqbot"

@classmethod
def _missing_(cls, value):
"""Custom implementation for handling missing values.
the method parses any missing values and only allows the ones that start with "initialtracer".
"""
# Allow strings starting with "tracer"
if isinstance(value, str) and value.startswith(
INITIAL_CONDITION_QUANTITIES_VALID_PREFIXES
):
new_member = str.__new__(cls, value)
new_member._value_ = value
return new_member
else:
raise ValueError(
f"{value} is not a valid {cls.__name__} possible quantities are {', '.join(cls.__members__)}, "
f"and quantities that start with 'tracer'"
)


class ExtOldQuantity(StrEnum):
"""Enum class containing the valid values for the boundary conditions category
of the external forcings.
Expand Down Expand Up @@ -459,7 +523,7 @@ class ExtOldForcing(BaseModel):

filetype: ExtOldFileType = Field(alias="FILETYPE")
"""FileType: Indication of the file type.
Options:
1. Time series
2. Time series magnitude and direction
Expand All @@ -476,7 +540,7 @@ class ExtOldForcing(BaseModel):

method: ExtOldMethod = Field(alias="METHOD")
"""ExtOldMethod: The method of interpolation.
Options:
1. Pass through (no interpolation)
2. Interpolate time and space
Expand All @@ -502,8 +566,8 @@ class ExtOldForcing(BaseModel):

operand: Operand = Field(alias="OPERAND")
"""Operand: The operand to use for adding the provided values.
Options:
Options:
'O' Existing values are overwritten with the provided values.
'A' Provided values are used where existing values are missing.
'+' Existing values are summed with the provided values.
Expand Down Expand Up @@ -685,9 +749,9 @@ class ExtOldModel(ParsableFileModel):
This model is typically referenced under a [FMModel][hydrolib.core.dflowfm.mdu.models.FMModel]`.external_forcing.extforcefile`.
"""

comment: List[str] = HEADER.splitlines()[1:]
comment: List[str] = Field(default=HEADER.splitlines()[1:])
"""List[str]: The comments in the header of the external forcing file."""
forcing: List[ExtOldForcing] = []
forcing: List[ExtOldForcing] = Field(default_factory=list)
"""List[ExtOldForcing]: The external forcing/QUANTITY blocks in the external forcing file."""

@classmethod
Expand Down
4 changes: 3 additions & 1 deletion hydrolib/core/dflowfm/ini/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ def _to_section(
props.append(prop)
return Section(header=self._header, content=props)

def _should_be_serialized(self, key: str, value: Any, save_settings: ModelSaveSettings) -> bool:
def _should_be_serialized(
self, key: str, value: Any, save_settings: ModelSaveSettings
) -> bool:
if key in self._exclude_fields():
return False

Expand Down
4 changes: 2 additions & 2 deletions hydrolib/core/dflowfm/inifield/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ class IniFieldModel(INIModel):
"""

general: IniFieldGeneral = IniFieldGeneral()
initial: List[InitialField] = []
parameter: List[ParameterField] = []
initial: List[InitialField] = Field(default_factory=list)
parameter: List[ParameterField] = Field(default_factory=list)

_split_to_list = make_list_validator("initial", "parameter")

Expand Down
18 changes: 10 additions & 8 deletions hydrolib/core/dimr/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class Component(BaseModel, ABC):
workingDir: Path
inputFile: Path
process: Optional[int]
setting: Optional[List[KeyValuePair]] = []
parameter: Optional[List[KeyValuePair]] = []
setting: Optional[List[KeyValuePair]] = Field(default_factory=list)
parameter: Optional[List[KeyValuePair]] = Field(default_factory=list)
mpiCommunicator: Optional[str]

model: Optional[FileModel]
Expand Down Expand Up @@ -225,7 +225,7 @@ class Coupler(BaseModel):
name: str
sourceComponent: str
targetComponent: str
item: List[CoupledItem] = []
item: List[CoupledItem] = Field(default_factory=list)
logger: Optional[Logger]

@validator("item", pre=True)
Expand All @@ -252,8 +252,8 @@ class StartGroup(BaseModel):
"""

time: str
start: List[ComponentOrCouplerRef] = []
coupler: List[ComponentOrCouplerRef] = []
start: List[ComponentOrCouplerRef] = Field(default_factory=list)
coupler: List[ComponentOrCouplerRef] = Field(default_factory=list)

@validator("start", "coupler", pre=True)
def validate_start(cls, v):
Expand Down Expand Up @@ -336,9 +336,11 @@ class DIMR(ParsableFileModel):
"""

documentation: Documentation = Documentation()
control: List[Union[Start, Parallel]] = Field([])
component: List[Union[RRComponent, FMComponent, Component]] = []
coupler: Optional[List[Coupler]] = []
control: List[Union[Start, Parallel]] = Field(default_factory=list)
component: List[Union[RRComponent, FMComponent, Component]] = Field(
default_factory=list
)
coupler: Optional[List[Coupler]] = Field(default_factory=list)
waitFile: Optional[str]
global_settings: Optional[GlobalSettings]

Expand Down
10 changes: 9 additions & 1 deletion hydrolib/tools/ext_old_to_new/converter_factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from hydrolib.core.dflowfm.extold.models import ExtOldMeteoQuantity
from hydrolib.core.dflowfm.extold.models import (
ExtOldInitialConditionQuantity,
ExtOldMeteoQuantity,
)
from hydrolib.tools.ext_old_to_new.initial_condition_converter import (
InitialConditionConverter,
)

from .base_converter import BaseConverter
from .meteo_converter import MeteoConverter
Expand Down Expand Up @@ -34,5 +40,7 @@ def create_converter(quantity) -> BaseConverter:
"""
if __contains__(ExtOldMeteoQuantity, quantity):
return MeteoConverter()
if __contains__(ExtOldInitialConditionQuantity, quantity):
return InitialConditionConverter()
else:
raise ValueError(f"No converter available for QUANTITY={quantity}.")
75 changes: 75 additions & 0 deletions hydrolib/tools/ext_old_to_new/initial_condition_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from hydrolib.core.basemodel import DiskOnlyFileModel
from hydrolib.core.dflowfm.extold.models import ExtOldForcing
from hydrolib.core.dflowfm.inifield.models import InitialField, InterpolationMethod
from hydrolib.tools.ext_old_to_new.base_converter import BaseConverter
from hydrolib.tools.ext_old_to_new.enum_converters import (
oldfiletype_to_forcing_file_type,
oldmethod_to_averaging_type,
oldmethod_to_interpolation_method,
)


class InitialConditionConverter(BaseConverter):
def __init__(self):
super().__init__()

def convert(self, forcing: ExtOldForcing) -> InitialField:
"""Convert an old external forcing block with Initial condition data to a InitialField
forcing block suitable for inclusion in a new inifieldfile file.
This function takes a forcing block from an old external forcings
file, represented by an instance of ExtOldForcing, and converts it
into a InitialField object. The InitialField object is suitable for use in new
iniFieldFile, adhering to the updated format and specifications.
Args:
forcing (ExtOldForcing): The contents of a single forcing block
in an old external forcings file. This object contains all the
necessary information, such as quantity, values, and timestamps,
required for the conversion process.
Returns:
Initial condition field definition, represents an `[Initial]` block in an inifield file.
Raises:
ValueError: If the forcing block contains a quantity that is not
supported by the converter, a ValueError is raised. This ensures
that only compatible forcing blocks are processed, maintaining
data integrity and preventing errors in the conversion process.
References:
[Sec.D](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.D)
"""
block_data = {
"quantity": forcing.quantity,
"datafile": forcing.filename,
"datafiletype": oldfiletype_to_forcing_file_type(forcing.filetype),
}
if block_data["datafiletype"] == "polygon":
block_data["value"] = forcing.value

if forcing.sourcemask != DiskOnlyFileModel(None):
raise ValueError(
f"Attribute 'SOURCEMASK' is no longer supported, cannot "
f"convert this input. Encountered for QUANTITY="
f"{forcing.quantity} and FILENAME={forcing.filename}."
)
block_data["interpolationmethod"] = oldmethod_to_interpolation_method(
forcing.method
)
if block_data["interpolationmethod"] == InterpolationMethod.averaging:
block_data["averagingtype"] = oldmethod_to_averaging_type(forcing.method)
block_data["averagingrelsize"] = forcing.relativesearchcellsize
block_data["averagingnummin"] = forcing.nummin
block_data["averagingpercentile"] = forcing.percentileminmax
block_data["operand"] = forcing.operand

if hasattr(forcing, "extrapolation"):
block_data["extrapolationmethod"] = forcing.extrapolation
if hasattr(forcing, "locationtype"):
block_data["locationtype"] = forcing.locationtype

new_block = InitialField(**block_data)

return new_block
Loading

0 comments on commit 037cbc3

Please sign in to comment.