Skip to content

Commit 037cbc3

Browse files
authored
feat: initial condition converter from old external forcing file
feat: initial condition converter from old external forcing file
1 parent 2fb8c8b commit 037cbc3

29 files changed

+2977
-1131
lines changed

.flake8

Lines changed: 0 additions & 1 deletion
This file was deleted.

hydrolib/core/basemodel.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,9 @@ class ModelSaveSettings:
513513

514514
_os_path_style = get_path_style_for_current_operating_system()
515515

516-
def __init__(self, path_style: Optional[PathStyle] = None, exclude_unset: bool = False) -> None:
516+
def __init__(
517+
self, path_style: Optional[PathStyle] = None, exclude_unset: bool = False
518+
) -> None:
517519
"""Initializes a new instance of the ModelSaveSettings class.
518520
519521
Args:
@@ -1054,7 +1056,9 @@ def save(
10541056
self.filepath = filepath
10551057

10561058
path_style = path_style_validator.validate(path_style)
1057-
save_settings = ModelSaveSettings(path_style=path_style, exclude_unset=exclude_unset)
1059+
save_settings = ModelSaveSettings(
1060+
path_style=path_style, exclude_unset=exclude_unset
1061+
)
10581062

10591063
# Handle save
10601064
with file_load_context() as context:

hydrolib/core/dflowfm/ext/models.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@
1010
)
1111
from hydrolib.core.dflowfm.bc.models import ForcingBase, ForcingData, ForcingModel
1212
from hydrolib.core.dflowfm.common.models import Operand
13-
from hydrolib.core.dflowfm.ini.models import (
14-
INIBasedModel,
15-
INIGeneral,
16-
INIModel,
17-
INISerializerConfig,
18-
)
13+
from hydrolib.core.dflowfm.ini.models import INIBasedModel, INIGeneral, INIModel
1914
from hydrolib.core.dflowfm.ini.serializer import INISerializerConfig
2015
from hydrolib.core.dflowfm.ini.util import (
2116
LocationValidationConfiguration,
@@ -102,7 +97,7 @@ def _get_identifier(self, data: dict) -> Optional[str]:
10297
return data.get("nodeid")
10398

10499
@property
105-
def forcing(self) -> ForcingBase:
100+
def forcing(self) -> Union[ForcingBase, None]:
106101
"""Retrieves the corresponding forcing data for this boundary.
107102
108103
Returns:
@@ -214,7 +209,7 @@ class MeteoForcingFileType(StrEnum):
214209
netcdf = "netcdf"
215210
"""str: NetCDF, either with gridded data, or multiple station time series."""
216211

217-
allowedvaluestext = "Possible values: bcAscii, netcdf, uniform."
212+
allowedvaluestext = "Possible values: bcAscii, uniform, uniMagDir, meteoGridEqui, spiderweb, meteoGridCurvi, netcdf."
218213

219214

220215
class MeteoInterpolationMethod(StrEnum):
@@ -224,10 +219,10 @@ class MeteoInterpolationMethod(StrEnum):
224219
"""
225220

226221
nearestnb = "nearestNb"
227-
linearSpaceTime = "linearSpaceTime"
228222
"""str: Nearest-neighbour interpolation, only with station-data in forcingFileType=netcdf"""
229-
230-
allowedvaluestext = "Possible values: nearestNb (only with station data in forcingFileType=netcdf ). "
223+
linearSpaceTime = "linearSpaceTime"
224+
"""str: Linear interpolation in space and time."""
225+
allowedvaluestext = "Possible values: nearestNb, linearSpaceTime."
231226

232227

233228
class Meteo(INIBasedModel):
@@ -345,9 +340,9 @@ class ExtModel(INIModel):
345340
"""
346341

347342
general: ExtGeneral = ExtGeneral()
348-
boundary: List[Boundary] = []
349-
lateral: List[Lateral] = []
350-
meteo: List[Meteo] = []
343+
boundary: List[Boundary] = Field(default_factory=list)
344+
lateral: List[Lateral] = Field(default_factory=list)
345+
meteo: List[Meteo] = Field(default_factory=list)
351346
serializer_config: INISerializerConfig = INISerializerConfig(
352347
section_indent=0, property_indent=0
353348
)

hydrolib/core/dflowfm/extold/models.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818
from hydrolib.core.dflowfm.polyfile.models import PolyFile
1919
from hydrolib.core.dflowfm.tim.models import TimModel
2020

21+
INITIAL_CONDITION_QUANTITIES_VALID_PREFIXES = (
22+
"initialtracer",
23+
"initialsedfrac",
24+
"initialverticalsedfracprofile",
25+
"initialverticalsigmasedfracprofile",
26+
)
27+
28+
2129
HEADER = """
2230
QUANTITY : waterlevelbnd, velocitybnd, dischargebnd, tangentialvelocitybnd, normalvelocitybnd filetype=9 method=2,3
2331
: outflowbnd, neumannbnd, qhbnd, uxuyadvectionvelocitybnd filetype=9 method=2,3
@@ -151,7 +159,7 @@ class ExtOldBoundaryQuantity(StrEnum):
151159
NormalVelocityBnd = "normalvelocitybnd"
152160
"""Normal velocity"""
153161
TangentialVelocityBnd = "tangentialvelocitybnd"
154-
"""Tangentional velocity"""
162+
"""Tangential velocity"""
155163
QhBnd = "qhbnd"
156164
"""Discharge-water level dependency"""
157165

@@ -223,6 +231,62 @@ class ExtOldMeteoQuantity(StrEnum):
223231
"""Dewpoint temperature"""
224232

225233

234+
class ExtOldInitialConditionQuantity(StrEnum):
235+
"""
236+
Initial Condition quantities:
237+
initialwaterlevel, initialsalinity, initialsalinitytop, initialtemperature,
238+
initialverticaltemperatureprofile, initialverticalsalinityprofile, initialvelocityx,
239+
initialvelocityy, initialvelocity
240+
241+
If there is a missing quantity that is mentioned in the "Accepted quantity names" section of the user manual
242+
[Sec.C.5.3](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.C.5.3).
243+
and [Sec.D.3](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.D.3).
244+
please open and issue in github.
245+
"""
246+
247+
# Initial Condition fields
248+
BedLevel = "bedlevel"
249+
BedLevel1D = "bedlevel1D"
250+
BedLevel2D = "bedlevel2D"
251+
252+
InitialWaterLevel = "initialwaterlevel"
253+
InitialWaterLevel1D = "initialwaterlevel1d"
254+
InitialWaterLevel2D = "initialwaterlevel2d"
255+
256+
InitialSalinity = "initialsalinity"
257+
InitialSalinityTop = "initialsalinitytop"
258+
InitialSalinityBot = "initialsalinitybot"
259+
InitialVerticalSalinityProfile = "initialverticalsalinityprofile"
260+
261+
InitialTemperature = "initialtemperature"
262+
InitialVerticalTemperatureProfile = "initialverticaltemperatureprofile"
263+
264+
initialUnsaturatedZoneThickness = "initialunsaturatedzonethickness"
265+
InitialVelocityX = "initialvelocityx"
266+
InitialVelocityY = "initialvelocityy"
267+
InitialVelocity = "initialvelocity"
268+
InitialWaqBot = "initialwaqbot"
269+
270+
@classmethod
271+
def _missing_(cls, value):
272+
"""Custom implementation for handling missing values.
273+
274+
the method parses any missing values and only allows the ones that start with "initialtracer".
275+
"""
276+
# Allow strings starting with "tracer"
277+
if isinstance(value, str) and value.startswith(
278+
INITIAL_CONDITION_QUANTITIES_VALID_PREFIXES
279+
):
280+
new_member = str.__new__(cls, value)
281+
new_member._value_ = value
282+
return new_member
283+
else:
284+
raise ValueError(
285+
f"{value} is not a valid {cls.__name__} possible quantities are {', '.join(cls.__members__)}, "
286+
f"and quantities that start with 'tracer'"
287+
)
288+
289+
226290
class ExtOldQuantity(StrEnum):
227291
"""Enum class containing the valid values for the boundary conditions category
228292
of the external forcings.
@@ -459,7 +523,7 @@ class ExtOldForcing(BaseModel):
459523

460524
filetype: ExtOldFileType = Field(alias="FILETYPE")
461525
"""FileType: Indication of the file type.
462-
526+
463527
Options:
464528
1. Time series
465529
2. Time series magnitude and direction
@@ -476,7 +540,7 @@ class ExtOldForcing(BaseModel):
476540

477541
method: ExtOldMethod = Field(alias="METHOD")
478542
"""ExtOldMethod: The method of interpolation.
479-
543+
480544
Options:
481545
1. Pass through (no interpolation)
482546
2. Interpolate time and space
@@ -502,8 +566,8 @@ class ExtOldForcing(BaseModel):
502566

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

688-
comment: List[str] = HEADER.splitlines()[1:]
752+
comment: List[str] = Field(default=HEADER.splitlines()[1:])
689753
"""List[str]: The comments in the header of the external forcing file."""
690-
forcing: List[ExtOldForcing] = []
754+
forcing: List[ExtOldForcing] = Field(default_factory=list)
691755
"""List[ExtOldForcing]: The external forcing/QUANTITY blocks in the external forcing file."""
692756

693757
@classmethod

hydrolib/core/dflowfm/ini/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ def _to_section(
232232
props.append(prop)
233233
return Section(header=self._header, content=props)
234234

235-
def _should_be_serialized(self, key: str, value: Any, save_settings: ModelSaveSettings) -> bool:
235+
def _should_be_serialized(
236+
self, key: str, value: Any, save_settings: ModelSaveSettings
237+
) -> bool:
236238
if key in self._exclude_fields():
237239
return False
238240

hydrolib/core/dflowfm/inifield/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,8 @@ class IniFieldModel(INIModel):
216216
"""
217217

218218
general: IniFieldGeneral = IniFieldGeneral()
219-
initial: List[InitialField] = []
220-
parameter: List[ParameterField] = []
219+
initial: List[InitialField] = Field(default_factory=list)
220+
parameter: List[ParameterField] = Field(default_factory=list)
221221

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

hydrolib/core/dimr/models.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class Component(BaseModel, ABC):
5353
workingDir: Path
5454
inputFile: Path
5555
process: Optional[int]
56-
setting: Optional[List[KeyValuePair]] = []
57-
parameter: Optional[List[KeyValuePair]] = []
56+
setting: Optional[List[KeyValuePair]] = Field(default_factory=list)
57+
parameter: Optional[List[KeyValuePair]] = Field(default_factory=list)
5858
mpiCommunicator: Optional[str]
5959

6060
model: Optional[FileModel]
@@ -225,7 +225,7 @@ class Coupler(BaseModel):
225225
name: str
226226
sourceComponent: str
227227
targetComponent: str
228-
item: List[CoupledItem] = []
228+
item: List[CoupledItem] = Field(default_factory=list)
229229
logger: Optional[Logger]
230230

231231
@validator("item", pre=True)
@@ -252,8 +252,8 @@ class StartGroup(BaseModel):
252252
"""
253253

254254
time: str
255-
start: List[ComponentOrCouplerRef] = []
256-
coupler: List[ComponentOrCouplerRef] = []
255+
start: List[ComponentOrCouplerRef] = Field(default_factory=list)
256+
coupler: List[ComponentOrCouplerRef] = Field(default_factory=list)
257257

258258
@validator("start", "coupler", pre=True)
259259
def validate_start(cls, v):
@@ -336,9 +336,11 @@ class DIMR(ParsableFileModel):
336336
"""
337337

338338
documentation: Documentation = Documentation()
339-
control: List[Union[Start, Parallel]] = Field([])
340-
component: List[Union[RRComponent, FMComponent, Component]] = []
341-
coupler: Optional[List[Coupler]] = []
339+
control: List[Union[Start, Parallel]] = Field(default_factory=list)
340+
component: List[Union[RRComponent, FMComponent, Component]] = Field(
341+
default_factory=list
342+
)
343+
coupler: Optional[List[Coupler]] = Field(default_factory=list)
342344
waitFile: Optional[str]
343345
global_settings: Optional[GlobalSettings]
344346

hydrolib/tools/ext_old_to_new/converter_factory.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from hydrolib.core.dflowfm.extold.models import ExtOldMeteoQuantity
1+
from hydrolib.core.dflowfm.extold.models import (
2+
ExtOldInitialConditionQuantity,
3+
ExtOldMeteoQuantity,
4+
)
5+
from hydrolib.tools.ext_old_to_new.initial_condition_converter import (
6+
InitialConditionConverter,
7+
)
28

39
from .base_converter import BaseConverter
410
from .meteo_converter import MeteoConverter
@@ -34,5 +40,7 @@ def create_converter(quantity) -> BaseConverter:
3440
"""
3541
if __contains__(ExtOldMeteoQuantity, quantity):
3642
return MeteoConverter()
43+
if __contains__(ExtOldInitialConditionQuantity, quantity):
44+
return InitialConditionConverter()
3745
else:
3846
raise ValueError(f"No converter available for QUANTITY={quantity}.")
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from hydrolib.core.basemodel import DiskOnlyFileModel
2+
from hydrolib.core.dflowfm.extold.models import ExtOldForcing
3+
from hydrolib.core.dflowfm.inifield.models import InitialField, InterpolationMethod
4+
from hydrolib.tools.ext_old_to_new.base_converter import BaseConverter
5+
from hydrolib.tools.ext_old_to_new.enum_converters import (
6+
oldfiletype_to_forcing_file_type,
7+
oldmethod_to_averaging_type,
8+
oldmethod_to_interpolation_method,
9+
)
10+
11+
12+
class InitialConditionConverter(BaseConverter):
13+
def __init__(self):
14+
super().__init__()
15+
16+
def convert(self, forcing: ExtOldForcing) -> InitialField:
17+
"""Convert an old external forcing block with Initial condition data to a InitialField
18+
forcing block suitable for inclusion in a new inifieldfile file.
19+
20+
21+
This function takes a forcing block from an old external forcings
22+
file, represented by an instance of ExtOldForcing, and converts it
23+
into a InitialField object. The InitialField object is suitable for use in new
24+
iniFieldFile, adhering to the updated format and specifications.
25+
26+
Args:
27+
forcing (ExtOldForcing): The contents of a single forcing block
28+
in an old external forcings file. This object contains all the
29+
necessary information, such as quantity, values, and timestamps,
30+
required for the conversion process.
31+
32+
Returns:
33+
Initial condition field definition, represents an `[Initial]` block in an inifield file.
34+
35+
Raises:
36+
ValueError: If the forcing block contains a quantity that is not
37+
supported by the converter, a ValueError is raised. This ensures
38+
that only compatible forcing blocks are processed, maintaining
39+
data integrity and preventing errors in the conversion process.
40+
41+
References:
42+
[Sec.D](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.D)
43+
"""
44+
block_data = {
45+
"quantity": forcing.quantity,
46+
"datafile": forcing.filename,
47+
"datafiletype": oldfiletype_to_forcing_file_type(forcing.filetype),
48+
}
49+
if block_data["datafiletype"] == "polygon":
50+
block_data["value"] = forcing.value
51+
52+
if forcing.sourcemask != DiskOnlyFileModel(None):
53+
raise ValueError(
54+
f"Attribute 'SOURCEMASK' is no longer supported, cannot "
55+
f"convert this input. Encountered for QUANTITY="
56+
f"{forcing.quantity} and FILENAME={forcing.filename}."
57+
)
58+
block_data["interpolationmethod"] = oldmethod_to_interpolation_method(
59+
forcing.method
60+
)
61+
if block_data["interpolationmethod"] == InterpolationMethod.averaging:
62+
block_data["averagingtype"] = oldmethod_to_averaging_type(forcing.method)
63+
block_data["averagingrelsize"] = forcing.relativesearchcellsize
64+
block_data["averagingnummin"] = forcing.nummin
65+
block_data["averagingpercentile"] = forcing.percentileminmax
66+
block_data["operand"] = forcing.operand
67+
68+
if hasattr(forcing, "extrapolation"):
69+
block_data["extrapolationmethod"] = forcing.extrapolation
70+
if hasattr(forcing, "locationtype"):
71+
block_data["locationtype"] = forcing.locationtype
72+
73+
new_block = InitialField(**block_data)
74+
75+
return new_block

0 commit comments

Comments
 (0)