Skip to content

Commit c90b354

Browse files
committed
Merge branch 'feat/hydrolib-core' (v0.1.0a2)
Brings the official Deltares Python stack integration into main: - feat(hydrolib): optional integration with hydrolib-core 1.0 - feat(viz): U/V vector overlay + dfm-tools integration - feat(mesh): MeshKernel/xugrid adapter + wireframe overlay - chore(release): v0.1.0a2 129 tests passing, ruff + mypy strict green.
2 parents ca18d73 + f94513c commit c90b354

15 files changed

Lines changed: 1842 additions & 11 deletions

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ body:
4747
attributes:
4848
label: DeltaSuite version
4949
description: Output of `deltasuite --version` (or the value shown in Help → About).
50-
placeholder: "0.1.0a1"
50+
placeholder: "0.1.0a2"
5151
validations:
5252
required: true
5353

CHANGELOG.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.0a2] - 2026-05-13
11+
12+
Second alpha. Adds the official **Deltares Python stack** integration
13+
(`hydrolib-core`, `dfm-tools`, `meshkernel` / `xugrid`) and three new
14+
visible features in the Map tab built on top of those libraries.
15+
16+
### Added
17+
18+
- **hydrolib-core integration (`core/hydrolib_adapter.py`)**
19+
- `safe_load_fmmodel()` returns a `HydrolibLoadResult` that exposes
20+
either a typed `FMModel` (when hydrolib-core is installed) or a
21+
structured error, never raises through the GUI.
22+
- `fmmodel_section_summary()` / `fmmodel_set_values()` so the Setup
23+
tab can use the canonical schema without depending on the import
24+
happening at GUI startup.
25+
- Cached availability and version probes
26+
(`is_hydrolib_available`, `hydrolib_version`).
27+
- **dfm-tools integration (`core/dfm_tools_adapter.py`) + U/V vector
28+
overlay**
29+
- `open_partitioned_smart()` / `open_curvilinear_smart()` wrap
30+
`dfmt.open_partitioned_dataset` / `dfmt.open_dataset_curvilinear`
31+
with graceful fallbacks to plain xarray when dfm-tools is not
32+
installed.
33+
- `find_uv_variables()` and `extract_uv_field()` produce a
34+
library-agnostic `UVField` (x, y, u, v, magnitude, optional time).
35+
- `MapViewerWidget.set_vector_overlay()` draws the `quiver` on top
36+
of the colour mesh with configurable colour, scale and stride.
37+
- `ResultControls` exposes a *Show U/V vectors* row with a stride
38+
spin box that auto-disables when the dataset has no recognised
39+
velocity pair.
40+
- **MeshKernel / xugrid integration (`core/mesh_adapter.py`)**
41+
- Library-agnostic `MeshGeometry` dataclass (nodes, edges, optional
42+
face-node connectivity) and `MeshLoadResult` wrapper.
43+
- `load_mesh_from_dataset()` and `load_mesh_from_path()` with two
44+
backends: `xugrid` when installed (canonical UGRID parser), and a
45+
pure-numpy heuristic fallback that recognises the standard
46+
`mesh2d_*` variable names (with automatic 1-based → 0-based
47+
normalisation for both edge and face connectivity, preserving the
48+
`-1` sentinel for ragged faces).
49+
- Cached availability and version probes
50+
(`is_xugrid_available`, `xugrid_version`,
51+
`is_meshkernel_available`, `meshkernel_version`) so the GUI can
52+
enable / disable the wireframe row without paying the cost of
53+
importing the C++ extension.
54+
- All eight new symbols re-exported from `deltasuite.core`.
55+
- **Mesh wireframe overlay in the Map tab**
56+
- `MapViewerWidget.set_mesh_overlay()` adds a thin grey
57+
`LineCollection` of mesh edges below the U/V quiver (so arrows
58+
stay readable). Sentinel-padded edges (`-1` UGRID convention) are
59+
filtered out.
60+
- `ResultControls` exposes a *Show mesh wireframe* checkbox that
61+
auto-disables when the open dataset has no detectable UGRID mesh.
62+
- `ResultPanel` caches the parsed mesh per file so toggling the
63+
overlay does not re-open the dataset.
64+
- **9 new tests** (`test_mesh_adapter.py`) covering availability /
65+
version probes, missing-file handling, the xugrid path, the
66+
heuristic fallback, the `MeshGeometry` count properties and a
67+
full NetCDF round-trip on disk. Total: **129 tests** passing.
68+
1069
## [0.1.0a1] - 2026-05-11
1170

1271
First public alpha release. Ships everything needed to open, edit, run
@@ -231,5 +290,6 @@ they affect anyone reusing this build pipeline:
231290
- Sphinx documentation skeleton with auto-generated API reference
232291
- GPL-3.0 license, CONTRIBUTING and CODE_OF_CONDUCT
233292

234-
[Unreleased]: https://github.com/ghvmof/deltasuite/compare/v0.1.0a1...HEAD
293+
[Unreleased]: https://github.com/ghvmof/deltasuite/compare/v0.1.0a2...HEAD
294+
[0.1.0a2]: https://github.com/ghvmof/deltasuite/compare/v0.1.0a1...v0.1.0a2
235295
[0.1.0a1]: https://github.com/ghvmof/deltasuite/releases/tag/v0.1.0a1

pyproject.toml

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "deltasuite"
7-
version = "0.1.0a1"
7+
version = "0.1.0a2"
88
description = "Open source desktop suite for pre-processing, running and post-processing Delft3D models"
99
readme = "README.md"
1010
requires-python = ">=3.11"
@@ -81,10 +81,16 @@ science = [
8181
"pandas>=2.1",
8282
]
8383
delft3d = [
84-
"hydrolib-core>=0.7",
85-
"dfm-tools>=0.27",
86-
"meshkernel>=4.0",
87-
"xugrid>=0.10",
84+
# Official Deltares stack. ``hydrolib-core`` provides the canonical
85+
# parsers/serialisers for D-Flow FM (.mdu, structures, boundary conditions);
86+
# ``dfm-tools`` adds high-level post-processing (vector overlays,
87+
# cross-sections, partitioned datasets); ``meshkernel`` is the C++ mesh
88+
# engine that powers grid generation and editing; ``xugrid`` is the
89+
# xarray extension we use for UGRID-aware I/O.
90+
"hydrolib-core>=1.0",
91+
"dfm-tools>=0.45",
92+
"meshkernel>=8.2",
93+
"xugrid>=0.11",
8894
]
8995
dev = [
9096
"pytest>=8.0",
@@ -174,8 +180,12 @@ ignore = [
174180
"N803", # argument name should be lowercase (Qt uses camelCase)
175181
"N806", # variable in function should be lowercase (Qt uses camelCase)
176182
"PLC0415", # import-outside-top-level (we use lazy imports for Qt and to keep CLI snappy)
177-
"TCH001", # typing-only-first-party-import (we like having imports near usage)
178-
"TCH003", # typing-only-standard-library-import (extra noise for stdlib types)
183+
# NOTE: TCH001/TCH003 are the names used by the ruff version pinned in
184+
# pre-commit (0.5). Newer ruff (>=0.6) emits a deprecation warning and
185+
# remaps to TC001/TC003 automatically, but rejects TC001/TC003 in 0.5,
186+
# so we keep the old names here for compatibility with both.
187+
"TCH001",
188+
"TCH003",
179189
"SIM112", # uncapitalized environment variables (Windows uses 'ProgramFiles')
180190
]
181191

@@ -206,13 +216,27 @@ strict = true
206216
warn_unused_ignores = false
207217
warn_return_any = true
208218
ignore_missing_imports = true
219+
# Disable mypy from following imports into third-party packages that ship
220+
# Python 3.12+ syntax (``zarr``, ``dask``, etc.) - we already have
221+
# ``ignore_missing_imports = true`` for stub-less packages, but mypy will
222+
# try to actually parse them when they are available; ``follow_imports =
223+
# silent`` keeps mypy from descending into their source files.
224+
follow_imports = "silent"
209225
plugins = ["pydantic.mypy"]
210226
exclude = ["build", "dist", "docs/_build"]
211227

212228
[[tool.mypy.overrides]]
213229
module = "tests.*"
214230
disallow_untyped_defs = false
215231

232+
# Third-party packages whose source uses Python 3.12+ syntax (PEP 695 type
233+
# parameter lists, etc.) which our ``python_version = "3.11"`` mypy refuses
234+
# to parse. We do not call into these directly with explicit type
235+
# annotations, so silencing their analysis is harmless.
236+
[[tool.mypy.overrides]]
237+
module = ["zarr.*", "numcodecs.*", "donfig.*"]
238+
follow_imports = "skip"
239+
216240
# ----------------------------------------------------------------------
217241
# Pytest
218242
# ----------------------------------------------------------------------

src/deltasuite/core/__init__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,39 @@
1212
ConfigEntry,
1313
ConfigFormat,
1414
ConfigSection,
15+
SmartLoadResult,
16+
load_smart,
17+
)
18+
from deltasuite.core.dfm_tools_adapter import (
19+
DfmDatasetResult,
20+
UVField,
21+
dfm_tools_version,
22+
extract_uv_field,
23+
find_uv_variables,
24+
is_dfm_tools_available,
25+
open_curvilinear_smart,
26+
open_partitioned_smart,
27+
)
28+
from deltasuite.core.hydrolib_adapter import (
29+
HydrolibLoadResult,
30+
fmmodel_section_summary,
31+
fmmodel_set_values,
32+
hydrolib_version,
33+
is_hydrolib_available,
34+
safe_load_fmmodel,
1535
)
1636
from deltasuite.core.kernels import KernelInfo, KernelKind, KernelSet, detect_kernels
1737
from deltasuite.core.logging_setup import configure_logging
38+
from deltasuite.core.mesh_adapter import (
39+
MeshGeometry,
40+
MeshLoadResult,
41+
is_meshkernel_available,
42+
is_xugrid_available,
43+
load_mesh_from_dataset,
44+
load_mesh_from_path,
45+
meshkernel_version,
46+
xugrid_version,
47+
)
1848
from deltasuite.core.paths import AppPaths, get_app_paths
1949
from deltasuite.core.project import Project, ProjectMeta, ProjectType
2050
from deltasuite.core.project_detector import (
@@ -56,12 +86,16 @@
5686
"ConfigFormat",
5787
"ConfigSection",
5888
"DetectedProject",
89+
"DfmDatasetResult",
5990
"Field2D",
6091
"Grid2D",
6192
"GridKind",
93+
"HydrolibLoadResult",
6294
"KernelInfo",
6395
"KernelKind",
6496
"KernelSet",
97+
"MeshGeometry",
98+
"MeshLoadResult",
6599
"Project",
66100
"ProjectMeta",
67101
"ProjectType",
@@ -73,22 +107,42 @@
73107
"RunConfig",
74108
"RunConfigError",
75109
"Settings",
110+
"SmartLoadResult",
76111
"StationSeries",
77112
"TimeSeriesDataset",
78113
"TimeSeriesFile",
79114
"TimeSeriesVariable",
115+
"UVField",
80116
"build_run_config",
81117
"configure_logging",
82118
"detect_kernels",
83119
"detect_project",
120+
"dfm_tools_version",
84121
"discover_projects",
122+
"extract_uv_field",
85123
"find_history_files",
86124
"find_result_files",
125+
"find_uv_variables",
126+
"fmmodel_section_summary",
127+
"fmmodel_set_values",
87128
"get_app_paths",
88129
"get_recent",
89130
"get_settings",
131+
"hydrolib_version",
132+
"is_dfm_tools_available",
133+
"is_hydrolib_available",
134+
"is_meshkernel_available",
135+
"is_xugrid_available",
136+
"load_mesh_from_dataset",
137+
"load_mesh_from_path",
138+
"load_smart",
139+
"meshkernel_version",
90140
"open_bundled_sample",
141+
"open_curvilinear_smart",
142+
"open_partitioned_smart",
91143
"push_recent",
144+
"safe_load_fmmodel",
92145
"save_recent",
93146
"save_settings",
147+
"xugrid_version",
94148
]

src/deltasuite/core/config_files.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ def parse_mdf(text: str, *, path: Path | None = None) -> ConfigDocument:
276276
entry = ConfigEntry(key=key.strip(), value=value.strip(), comment="")
277277
section.entries.append(entry)
278278
last_entry = entry
279-
# Continuation line for the previous multi-value entry.
280279
elif last_entry is not None:
281280
last_entry.value = (last_entry.value + "\n" + line.strip()).strip()
282281
else:
@@ -305,12 +304,58 @@ def serialize_mdf(doc: ConfigDocument) -> str:
305304
return "\n".join(lines).rstrip() + "\n"
306305

307306

307+
# ---------------------------------------------------------------------------
308+
# Smart load helpers (optional ``hydrolib-core`` integration)
309+
# ---------------------------------------------------------------------------
310+
311+
312+
@dataclass(slots=True)
313+
class SmartLoadResult:
314+
"""Combined result of a "best effort" .mdu / .mdf load.
315+
316+
``document`` is always populated (we fall back to our lossless parser even
317+
when hydrolib-core rejects the file). ``hydrolib_validated`` reflects
318+
whether the file was *also* successfully validated against the official
319+
Deltares schema; ``hydrolib_error`` carries the validation message
320+
otherwise.
321+
"""
322+
323+
document: ConfigDocument
324+
hydrolib_validated: bool = False
325+
hydrolib_error: str | None = None
326+
327+
328+
def load_smart(path: Path) -> SmartLoadResult:
329+
"""Load a configuration file with extra validation when available.
330+
331+
For ``.mdu`` files this attempts a parallel parse with ``hydrolib-core``
332+
(when installed) and surfaces any validation errors without losing the
333+
primary, comment-preserving document. For ``.mdf`` files only our
334+
parser runs (hydrolib-core does not handle Delft3D 4 ``.mdf``).
335+
"""
336+
document = ConfigDocument.load(path)
337+
if document.format is not ConfigFormat.MDU:
338+
return SmartLoadResult(document=document)
339+
340+
# Lazy import: only touch hydrolib_adapter when actually needed.
341+
from deltasuite.core.hydrolib_adapter import safe_load_fmmodel
342+
343+
result = safe_load_fmmodel(document.path)
344+
return SmartLoadResult(
345+
document=document,
346+
hydrolib_validated=result.ok,
347+
hydrolib_error=result.error,
348+
)
349+
350+
308351
__all__ = (
309352
"ConfigDocument",
310353
"ConfigEntry",
311354
"ConfigFormat",
312355
"ConfigSection",
356+
"SmartLoadResult",
313357
"detect_format",
358+
"load_smart",
314359
"parse_mdf",
315360
"parse_mdu",
316361
"serialize_mdf",

0 commit comments

Comments
 (0)