Skip to content

Commit 25bd8c5

Browse files
committed
merge changes from base branch
2 parents 540f4e4 + 1d4b3c8 commit 25bd8c5

File tree

19 files changed

+258
-5201
lines changed

19 files changed

+258
-5201
lines changed

hydrolib/core/dflowfm/ext/models.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def _get_identifier(self, data: dict) -> Optional[str]:
9797
return data.get("nodeid")
9898

9999
@property
100-
def forcing(self) -> ForcingBase:
100+
def forcing(self) -> Union[ForcingBase, None]:
101101
"""Retrieves the corresponding forcing data for this boundary.
102102
103103
Returns:
@@ -208,7 +208,7 @@ class MeteoForcingFileType(StrEnum):
208208
netcdf = "netcdf"
209209
"""str: NetCDF, either with gridded data, or multiple station time series."""
210210

211-
allowedvaluestext = "Possible values: bcAscii, netcdf, uniform."
211+
allowedvaluestext = "Possible values: bcAscii, uniform, uniMagDir, meteoGridEqui, spiderweb, meteoGridCurvi, netcdf."
212212

213213

214214
class MeteoInterpolationMethod(StrEnum):
@@ -218,10 +218,10 @@ class MeteoInterpolationMethod(StrEnum):
218218
"""
219219

220220
nearestnb = "nearestNb"
221-
linearSpaceTime = "linearSpaceTime"
222221
"""str: Nearest-neighbour interpolation, only with station-data in forcingFileType=netcdf"""
223-
224-
allowedvaluestext = "Possible values: nearestNb (only with station data in forcingFileType=netcdf ). "
222+
linearSpaceTime = "linearSpaceTime"
223+
"""str: Linear interpolation in space and time."""
224+
allowedvaluestext = "Possible values: nearestNb, linearSpaceTime."
225225

226226

227227
class Meteo(INIBasedModel):
@@ -286,7 +286,7 @@ def _get_unknown_keyword_error_manager(cls) -> Optional[UnknownKeywordErrorManag
286286
)
287287

288288
_header: Literal["Meteo"] = "Meteo"
289-
quantity: str = Field(alias="QUANTITY")
289+
quantity: str = Field(alias="quantity")
290290
forcingfile: Union[TimModel, ForcingModel, DiskOnlyFileModel] = Field(
291291
alias="forcingFile"
292292
)

hydrolib/core/dflowfm/extold/models.py

Lines changed: 47 additions & 5 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

@@ -243,21 +251,55 @@ class ExtOldInitialConditionQuantity(StrEnum):
243251
Initial Condition quantities:
244252
initialwaterlevel, initialsalinity, initialsalinitytop, initialtemperature,
245253
initialverticaltemperatureprofile, initialverticalsalinityprofile, initialvelocityx,
246-
initialvelocityy, initialvelocity, initialsalinitytopuse
254+
initialvelocityy, initialvelocity
255+
256+
If there is a missing quantity that is mentioned in the "Accepted quantity names" section of the user manual
257+
[Sec.C.5.3](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.C.5.3).
258+
and [Sec.D.3](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.D.3).
259+
please open and issue in github.
247260
"""
248261

249262
# Initial Condition fields
263+
BedLevel = "bedlevel"
264+
BedLevel1D = "bedlevel1D"
265+
BedLevel2D = "bedlevel2D"
266+
250267
InitialWaterLevel = "initialwaterlevel"
268+
InitialWaterLevel1D = "initialwaterlevel1d"
269+
InitialWaterLevel2D = "initialwaterlevel2d"
270+
251271
InitialSalinity = "initialsalinity"
252272
InitialSalinityTop = "initialsalinitytop"
273+
InitialSalinityBot = "initialsalinitybot"
274+
InitialVerticalSalinityProfile = "initialverticalsalinityprofile"
275+
253276
InitialTemperature = "initialtemperature"
254277
InitialVerticalTemperatureProfile = "initialverticaltemperatureprofile"
255-
InitialVerticalSalinityProfile = "initialverticalsalinityprofile"
256278

279+
initialUnsaturatedZoneThickness = "initialunsaturatedzonethickness"
257280
InitialVelocityX = "initialvelocityx"
258281
InitialVelocityY = "initialvelocityy"
259282
InitialVelocity = "initialvelocity"
260-
InitialSalinityTopUse = "initialsalinitytopuse"
283+
InitialWaqBot = "initialwaqbot"
284+
285+
@classmethod
286+
def _missing_(cls, value):
287+
"""Custom implementation for handling missing values.
288+
289+
the method parses any missing values and only allows the ones that start with "initialtracer".
290+
"""
291+
# Allow strings starting with "tracer"
292+
if isinstance(value, str) and value.startswith(
293+
INITIAL_CONDITION_QUANTITIES_VALID_PREFIXES
294+
):
295+
new_member = str.__new__(cls, value)
296+
new_member._value_ = value
297+
return new_member
298+
else:
299+
raise ValueError(
300+
f"{value} is not a valid {cls.__name__} possible quantities are {', '.join(cls.__members__)}, "
301+
f"and quantities that start with 'tracer'"
302+
)
261303

262304

263305
class ExtOldQuantity(StrEnum):
@@ -722,7 +764,7 @@ class ExtOldModel(ParsableFileModel):
722764
This model is typically referenced under a [FMModel][hydrolib.core.dflowfm.mdu.models.FMModel]`.external_forcing.extforcefile`.
723765
"""
724766

725-
comment: List[str] = Field(default_factory=lambda: HEADER.splitlines()[1:])
767+
comment: List[str] = Field(default=HEADER.splitlines()[1:])
726768
"""List[str]: The comments in the header of the external forcing file."""
727769
forcing: List[ExtOldForcing] = Field(default_factory=list)
728770
"""List[ExtOldForcing]: The external forcing/QUANTITY blocks in the external forcing file."""

hydrolib/tools/ext_old_to_new/converters.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ def create_convert_inputs(forcing: ExtOldForcing) -> Dict[str, str]:
186186
block_data["operand"] = forcing.operand
187187

188188
if hasattr(forcing, "extrapolation"):
189-
block_data["extrapolationmethod"] = forcing.extrapolation
190-
if hasattr(forcing, "locationtype"):
191-
block_data["locationtype"] = forcing.locationtype
189+
block_data["extrapolationmethod"] = (
190+
"yes" if forcing.extrapolation == 1 else "no"
191+
)
192192

193193
return block_data
194194

@@ -199,14 +199,14 @@ def __init__(self):
199199
super().__init__()
200200

201201
def convert(self, forcing: ExtOldForcing) -> InitialField:
202-
"""Convert an old external forcing block with meteo data to a Meteo
203-
forcing block suitable for inclusion in a new external forcings file.
202+
"""Convert an old external forcing block with Initial condition data to a IinitialField
203+
forcing block suitable for inclusion in a new inifieldfile file.
204+
204205
205206
This function takes a forcing block from an old external forcings
206207
file, represented by an instance of ExtOldForcing, and converts it
207-
into a Meteo object. The Meteo object is suitable for use in new
208-
external forcings files, adhering to the updated format and
209-
specifications.
208+
into a InitialField object. The InitialField object is suitable for use in new
209+
iniFieldFile, adhering to the updated format and specifications.
210210
211211
Args:
212212
forcing (ExtOldForcing): The contents of a single forcing block
@@ -215,16 +215,16 @@ def convert(self, forcing: ExtOldForcing) -> InitialField:
215215
required for the conversion process.
216216
217217
Returns:
218-
Meteo: A Meteo object that represents the converted forcing
219-
block, ready to be included in a new external forcings file. The
220-
Meteo object conforms to the new format specifications, ensuring
221-
compatibility with updated systems and models.
218+
Initial condition field definition, represents an `[Initial]` block in an inifield file.
222219
223220
Raises:
224221
ValueError: If the forcing block contains a quantity that is not
225222
supported by the converter, a ValueError is raised. This ensures
226223
that only compatible forcing blocks are processed, maintaining
227224
data integrity and preventing errors in the conversion process.
225+
226+
References:
227+
[Sec.D](https://content.oss.deltares.nl/delft3dfm1d2d/D-Flow_FM_User_Manual_1D2D.pdf#subsection.D)
228228
"""
229229
data = create_convert_inputs(forcing)
230230
new_block = InitialField(**data)

hydrolib/tools/ext_old_to_new/main_converter.py

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class ExternalForcingConverter:
3030

3131
def __init__(
3232
self,
33-
extold_model: ExtOldModel,
33+
extold_model: Union[PathOrStr, ExtOldModel],
3434
ext_model_path: PathOrStr = None,
3535
inifield_model_path: PathOrStr = None,
3636
structure_model_path: PathOrStr = None,
@@ -41,15 +41,14 @@ def __init__(
4141
old external forcing file, if no paths were given by the user for the new models.
4242
4343
Args:
44-
extold_model (ExtOldModel): object with all forcing blocks.
44+
extold_model (PathOrStr, ExtOldModel): ExtOldModel or path to the old external forcing file.
4545
ext_model_path (PathOrStr, optional): Path to the new external forcing file.
4646
inifield_model_path (PathOrStr, optional): Path to the initial field file.
4747
structure_model_path (PathOrStr, optional): Path to the structure file.
4848
"""
49-
if not isinstance(extold_model, ExtOldModel):
50-
raise ValueError(
51-
f"Expected an ExtOldModel object, got {type(extold_model)} instead."
52-
)
49+
if isinstance(extold_model, Path) or isinstance(extold_model, str):
50+
extold_model = self._read_old_file(extold_model)
51+
5352
self._extold_model = extold_model
5453
rdir = extold_model.filepath.parent
5554

@@ -128,8 +127,8 @@ def structure_model(self, path: PathOrStr):
128127
StructureModel, path
129128
)
130129

131-
@classmethod
132-
def read_old_file(cls, extoldfile: PathOrStr) -> "ExternalForcingConverter":
130+
@staticmethod
131+
def _read_old_file(extoldfile: PathOrStr) -> ExtOldModel:
133132
"""Read a legacy D-Flow FM external forcings file (.ext) into an
134133
ExtOldModel object.
135134
@@ -149,13 +148,18 @@ def read_old_file(cls, extoldfile: PathOrStr) -> "ExternalForcingConverter":
149148
ExternalForcingConverter: object with the old external forcing model and new external forcing, initial field
150149
"""
151150
global _verbose
151+
if not isinstance(extoldfile, Path):
152+
extoldfile = Path(extoldfile)
153+
154+
if not extoldfile.exists():
155+
raise FileNotFoundError(f"File not found: {extoldfile}")
152156

153157
extold_model = ExtOldModel(extoldfile)
154158

155159
if _verbose:
156160
print(f"Read {len(extold_model.forcing)} forcing blocks from {extoldfile}.")
157161

158-
return cls(extold_model)
162+
return extold_model
159163

160164
def update(
161165
self,
@@ -272,15 +276,12 @@ def ext_old_to_new_from_mdu(
272276

273277
workdir = fmmodel._resolved_filepath.parent
274278
os.chdir(workdir)
275-
if fmmodel.external_forcing.extforcefile is None:
276-
if _verbose:
277-
print(
278-
f"mdufile: {mdufile} does not contain an old style external forcing file"
279-
)
280-
return
281-
# Input file:
279+
if fmmodel.external_forcing.extforcefile is None and _verbose:
280+
print(f"mdufile: {mdufile} does not contain an old style external forcing file")
281+
return
282+
282283
extoldfile = fmmodel.external_forcing.extforcefile._resolved_filepath
283-
# Output files:
284+
284285
extfile = (
285286
fmmodel.external_forcing.extforcefilenew._resolved_filepath
286287
if fmmodel.external_forcing.extforcefilenew
@@ -297,11 +298,14 @@ def ext_old_to_new_from_mdu(
297298
else workdir / structurefile
298299
)
299300

300-
converter = ExternalForcingConverter.read_old_file(extoldfile)
301-
# The actual conversion:
302-
extold_model, ext_model, inifield_model, structure_model = converter.update(
303-
extfile, inifieldfile, structurefile, backup, postfix
301+
converter = ExternalForcingConverter(
302+
extoldfile, extfile, inifieldfile, structurefile
304303
)
304+
305+
# The actual conversion:
306+
ext_model, inifield_model, structure_model = converter.update(postfix)
307+
converter.save(backup)
308+
extold_model = converter.extold_model
305309
try:
306310
# And include the new files in the FM model:
307311
_ = ExtModel(extfile)
@@ -315,7 +319,8 @@ def ext_old_to_new_from_mdu(
315319
fmmodel.geometry.structurefile[0] = structure_model
316320

317321
# Save the updated FM model:
318-
backup_file(fmmodel.filepath, backup)
322+
if backup:
323+
backup_file(fmmodel.filepath)
319324
converted_mdufile = construct_filepath_with_postfix(mdufile, postfix)
320325
fmmodel.filepath = converted_mdufile
321326
fmmodel.save(recurse=False, exclude_unset=True)
@@ -424,19 +429,19 @@ def main(args=None):
424429
args.mdufile, **outfiles, backup=backup, postfix=args.postfix
425430
)
426431
elif args.extoldfile is not None:
427-
converter = ExternalForcingConverter.read_old_file(args.extoldfile)
428-
converter.ext_model = outfiles["extfile"]
429-
converter.inifield_model = outfiles["inifieldfile"]
430-
converter.structure_model = outfiles["structurefile"]
432+
converter = ExternalForcingConverter(
433+
args.extoldfile,
434+
outfiles["extfile"],
435+
outfiles["inifieldfile"],
436+
outfiles["structurefile"],
437+
)
431438
converter.update(postfix=args.postfix)
432439
converter.save(backup=backup)
433440
elif args.dir is not None:
434-
ext_old_to_new_dir_recursive(args.dir, backup=backup, postfix=args.postfix)
441+
ext_old_to_new_dir_recursive(args.dir, backup=backup)
435442
else:
436443
print("Error: no input specified. Use one of --mdufile, --extoldfile or --dir.")
437444

438-
# sys.exit(1)
439-
440445
print(_program + ": done")
441446

442447

0 commit comments

Comments
 (0)