bluemesh2d 0.1.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bluemesh2d/_version.py +24 -0
- bluemesh2d/aabb_tree/findball.py +121 -0
- bluemesh2d/aabb_tree/findtria.py +299 -0
- bluemesh2d/aabb_tree/maketree.py +147 -0
- bluemesh2d/aabb_tree/mapvert.py +72 -0
- bluemesh2d/aabb_tree/queryset.py +83 -0
- bluemesh2d/aabb_tree/scantree.py +124 -0
- bluemesh2d/dependencies.py +83 -0
- bluemesh2d/feedback.py +142 -0
- bluemesh2d/geom_util/boundary_util.py +216 -0
- bluemesh2d/geom_util/getiso.py +194 -0
- bluemesh2d/geom_util/poly_util.py +795 -0
- bluemesh2d/geom_util/proj_util.py +250 -0
- bluemesh2d/geomesh_util/border_util.py +283 -0
- bluemesh2d/geomesh_util/depth_field.py +333 -0
- bluemesh2d/geomesh_util/grd_util.py +1000 -0
- bluemesh2d/geomesh_util/interpolation_mesh.py +318 -0
- bluemesh2d/geomesh_util/merge_circumcenters.py +268 -0
- bluemesh2d/geomesh_util/water_polygon.py +406 -0
- bluemesh2d/hfun_util/build_hfun.py +589 -0
- bluemesh2d/hfun_util/hfun_dispersion.py +96 -0
- bluemesh2d/hfun_util/lfshfn.py +110 -0
- bluemesh2d/hfun_util/limhfn.py +65 -0
- bluemesh2d/hfun_util/make_constant_hfun.py +32 -0
- bluemesh2d/hfun_util/make_depth_hfun.py +51 -0
- bluemesh2d/hfun_util/smooth_and_precomput.py +188 -0
- bluemesh2d/hfun_util/trihfn.py +84 -0
- bluemesh2d/hjac_util/limgrad.py +105 -0
- bluemesh2d/mesh_ball/cdtbal1.py +38 -0
- bluemesh2d/mesh_ball/cdtbal2.py +104 -0
- bluemesh2d/mesh_ball/inv_2x2.py +61 -0
- bluemesh2d/mesh_ball/inv_3x3.py +72 -0
- bluemesh2d/mesh_ball/pwrbal2.py +134 -0
- bluemesh2d/mesh_ball/tribal2.py +27 -0
- bluemesh2d/mesh_cost/relhfn.py +62 -0
- bluemesh2d/mesh_cost/triang.py +59 -0
- bluemesh2d/mesh_cost/triarea.py +51 -0
- bluemesh2d/mesh_cost/trideg.py +44 -0
- bluemesh2d/mesh_cost/triscr.py +43 -0
- bluemesh2d/mesh_file/bnd_util.py +664 -0
- bluemesh2d/mesh_file/loadmsh.py +165 -0
- bluemesh2d/mesh_file/ugrid.py +308 -0
- bluemesh2d/mesh_util/cfmtri.py +118 -0
- bluemesh2d/mesh_util/deltri.py +131 -0
- bluemesh2d/mesh_util/idxtri.py +53 -0
- bluemesh2d/mesh_util/isfeat.py +90 -0
- bluemesh2d/mesh_util/minlen.py +46 -0
- bluemesh2d/mesh_util/setset.py +49 -0
- bluemesh2d/mesh_util/tricon.py +90 -0
- bluemesh2d/mesh_util/tridiv.py +187 -0
- bluemesh2d/meshgen.py +202 -0
- bluemesh2d/ortho_merge/constants.py +27 -0
- bluemesh2d/ortho_merge/geometry.py +64 -0
- bluemesh2d/ortho_merge/ortho_merge_iter.py +812 -0
- bluemesh2d/ortho_merge/orthogonalize.py +2989 -0
- bluemesh2d/pipeline.py +277 -0
- bluemesh2d/poly_data/airfoil.msh +958 -0
- bluemesh2d/poly_data/channel.msh +212 -0
- bluemesh2d/poly_data/islands.msh +13819 -0
- bluemesh2d/poly_data/lake.msh +612 -0
- bluemesh2d/poly_data/river.msh +690 -0
- bluemesh2d/poly_test/inpoly.py +105 -0
- bluemesh2d/poly_test/inpoly_mat.py +104 -0
- bluemesh2d/refine.py +972 -0
- bluemesh2d/smood.py +623 -0
- bluemesh2d/smooth.py +522 -0
- bluemesh2d/tricost.py +426 -0
- bluemesh2d/tridemo.py +723 -0
- bluemesh2d/triread.py +53 -0
- bluemesh2d-0.1.1.dev0.dist-info/METADATA +139 -0
- bluemesh2d-0.1.1.dev0.dist-info/RECORD +74 -0
- bluemesh2d-0.1.1.dev0.dist-info/WHEEL +5 -0
- bluemesh2d-0.1.1.dev0.dist-info/licenses/LICENSE +674 -0
- bluemesh2d-0.1.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Iterative pipeline: orthogonalize <-> merge_circumcenters (with optional recovery).
|
|
3
|
+
|
|
4
|
+
Used by :mod:`bluemesh2d.smood` as its orthogonalization/merge pipeline. The
|
|
5
|
+
underlying numeric orthogonality/small-link kernels live in
|
|
6
|
+
:mod:`bluemesh2d.ortho_merge.orthogonalize`.
|
|
7
|
+
|
|
8
|
+
Intended process (Delft3D-FM / dual mesh)
|
|
9
|
+
-----------------------------------------
|
|
10
|
+
1. **Orthogonalize** on a triangulation of the current (mixed) faces **consistent with**
|
|
11
|
+
``merge_circumcenters`` / UGRID export: quads ``[a,v1,b,v2]`` are split on diagonal
|
|
12
|
+
``(v1,v2)`` (see ``geomesh_util.grd_util.triangulate_mixed_face_row_to_tris``), **not**
|
|
13
|
+
fan-from-``a`` (diagonal ``(a,b)``), which skews the dual w.r.t. Delft3D-FM.
|
|
14
|
+
2. **Remove short flow links**: ``merge_circumcenters`` merges triangle pairs into quads
|
|
15
|
+
(skipped when ``merge_small_links=False``: the mesh then stays pure triangles).
|
|
16
|
+
3. Repeat; optional **recovery** cycles if ``max|cosφ|`` or small-link count still fails
|
|
17
|
+
the dual criteria (see ``require_both_criteria``).
|
|
18
|
+
4. With ``require_both_criteria=True``, **raise** if criteria are still not met after the
|
|
19
|
+
main and recovery cycles. Default **False** skips the global dual check and recovery.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Callable, List, Optional, Sequence, Tuple
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class OrthoMergeStats:
|
|
32
|
+
outer_iter: int
|
|
33
|
+
max_cosphi: float
|
|
34
|
+
n_small_flow_links: int
|
|
35
|
+
merged_this_iter: int
|
|
36
|
+
n_zones_orthogonalized: int
|
|
37
|
+
recovery: bool = False
|
|
38
|
+
recovery_iter: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _faces_from_face_nodes(face_nodes: np.ndarray) -> List[np.ndarray]:
|
|
42
|
+
"""
|
|
43
|
+
Convert internal UGRID `face_nodes` (0-based, invalid = -1) to a list of faces.
|
|
44
|
+
|
|
45
|
+
The returned faces contain only valid node indices (>=0) and keep original polygon
|
|
46
|
+
size (3 or 4+ depending on input).
|
|
47
|
+
"""
|
|
48
|
+
face_nodes = np.asarray(face_nodes, dtype=np.int64)
|
|
49
|
+
faces: List[np.ndarray] = []
|
|
50
|
+
for f in range(face_nodes.shape[0]):
|
|
51
|
+
nodes = face_nodes[f, :]
|
|
52
|
+
nodes = nodes[nodes >= 0]
|
|
53
|
+
if nodes.size >= 3:
|
|
54
|
+
faces.append(nodes.astype(np.int64, copy=True))
|
|
55
|
+
return faces
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _triangulate_faces_for_ortho(
|
|
59
|
+
vert_xy: np.ndarray, faces: Sequence[np.ndarray]
|
|
60
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
61
|
+
"""
|
|
62
|
+
Triangle rows for ``orthogonalize_tria_mesh``, aligned with ``merge_circumcenters`` quads
|
|
63
|
+
and NetCDF export: use :func:`~bluemesh2d.geomesh_util.grd_util.triangulate_mixed_face_row_to_tris`
|
|
64
|
+
so 4-node faces use diagonal ``(v1,v2)``, not fan-from-``a``.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
(tria, tri_face_id)
|
|
69
|
+
- tria: (T,3) triangle rows.
|
|
70
|
+
- tri_face_id: (T,) index into ``faces`` of the origin face of each row.
|
|
71
|
+
"""
|
|
72
|
+
from ..geomesh_util.grd_util import triangulate_mixed_face_row_to_tris
|
|
73
|
+
|
|
74
|
+
vert_xy = np.asarray(vert_xy, dtype=np.float64)
|
|
75
|
+
if vert_xy.ndim != 2 or vert_xy.shape[1] < 2:
|
|
76
|
+
raise ValueError("vert_xy must have shape (N, 2) or (N, >=2) for x,y")
|
|
77
|
+
xy = vert_xy[:, :2]
|
|
78
|
+
out: List[Tuple[int, int, int]] = []
|
|
79
|
+
face_ids: List[int] = []
|
|
80
|
+
for fid, nodes in enumerate(faces):
|
|
81
|
+
n = np.asarray(nodes, dtype=np.int64).reshape(-1)
|
|
82
|
+
if n.size < 3:
|
|
83
|
+
continue
|
|
84
|
+
tris = triangulate_mixed_face_row_to_tris(xy, n)
|
|
85
|
+
out.extend(tris)
|
|
86
|
+
face_ids.extend([fid] * len(tris))
|
|
87
|
+
if out:
|
|
88
|
+
return np.asarray(out, dtype=np.int64), np.asarray(face_ids, dtype=np.int64)
|
|
89
|
+
return np.empty((0, 3), dtype=np.int64), np.empty(0, dtype=np.int64)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _faces_with_ortho_topology(
|
|
93
|
+
faces: Sequence[np.ndarray],
|
|
94
|
+
tria_in: np.ndarray,
|
|
95
|
+
tria_out: np.ndarray,
|
|
96
|
+
tri_face_id: np.ndarray,
|
|
97
|
+
) -> Sequence[np.ndarray]:
|
|
98
|
+
"""
|
|
99
|
+
Propagate topology changes (edge flips) made by ``orthogonalize_tria_mesh``
|
|
100
|
+
on the triangle proxy back onto the mixed face list.
|
|
101
|
+
|
|
102
|
+
``orthogonalize_tria_mesh`` flips edges by rewriting the two triangle rows
|
|
103
|
+
in place, so rows of ``tria_out`` correspond 1:1 to rows of ``tria_in``.
|
|
104
|
+
Faces whose proxy rows are unchanged are kept as-is (including quads). A
|
|
105
|
+
quad whose proxy rows changed only by re-diagonalization (no external node
|
|
106
|
+
entered) is also kept — the 4-node face is the same, only the proxy
|
|
107
|
+
diagonal moved. Any other changed face is replaced by its flipped triangle
|
|
108
|
+
rows: without this, flips were silently discarded by the dataset rebuild
|
|
109
|
+
and the outer/recovery cycles could stall forever on an edge that only a
|
|
110
|
+
flip can fix, while the ortho stage kept reporting it as solved.
|
|
111
|
+
"""
|
|
112
|
+
if tria_out.shape != tria_in.shape:
|
|
113
|
+
# Unexpected: fall back to the original faces (previous behaviour).
|
|
114
|
+
return faces
|
|
115
|
+
row_changed = np.any(np.sort(tria_in, axis=1) != np.sort(tria_out, axis=1), axis=1)
|
|
116
|
+
if not np.any(row_changed):
|
|
117
|
+
return faces
|
|
118
|
+
|
|
119
|
+
changed_faces = set(int(f) for f in np.unique(tri_face_id[row_changed]))
|
|
120
|
+
faces_out: List[np.ndarray] = []
|
|
121
|
+
for fid, f in enumerate(faces):
|
|
122
|
+
if fid not in changed_faces:
|
|
123
|
+
faces_out.append(f)
|
|
124
|
+
continue
|
|
125
|
+
rows = np.where(tri_face_id == fid)[0]
|
|
126
|
+
face_nodes = set(int(v) for v in np.asarray(f).reshape(-1))
|
|
127
|
+
out_nodes = set(int(v) for v in tria_out[rows].ravel())
|
|
128
|
+
if len(f) >= 4 and out_nodes <= face_nodes:
|
|
129
|
+
# Internal re-diagonalization of a quad: same 4-node face.
|
|
130
|
+
faces_out.append(f)
|
|
131
|
+
else:
|
|
132
|
+
for r in rows:
|
|
133
|
+
faces_out.append(np.asarray(tria_out[r], dtype=np.int64).copy())
|
|
134
|
+
return faces_out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _face_nodes_raw_to_0b(face_nodes_raw: np.ndarray, start_index: int) -> np.ndarray:
|
|
138
|
+
"""UGRID face_nodes array to 0-based with -1 fill."""
|
|
139
|
+
face_nodes_raw = np.asarray(face_nodes_raw, dtype=np.int64)
|
|
140
|
+
if int(start_index) == 1:
|
|
141
|
+
face_nodes = np.full_like(face_nodes_raw, -1)
|
|
142
|
+
valid = face_nodes_raw > 0
|
|
143
|
+
face_nodes[valid] = face_nodes_raw[valid] - 1
|
|
144
|
+
return face_nodes
|
|
145
|
+
return face_nodes_raw.copy()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _fan_vert_tria_from_ds(
|
|
149
|
+
ds_cur,
|
|
150
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
151
|
+
"""
|
|
152
|
+
Triangle proxy of the mixed UGRID dataset, aligned with quad splitting
|
|
153
|
+
used by the rest of this repository.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
vert_xy : (N,2) float64
|
|
158
|
+
Triangle-proxy vertex coordinates.
|
|
159
|
+
tria : (T,3) int64
|
|
160
|
+
Triangle connectivity of the proxy.
|
|
161
|
+
tri_origin_face_id : (T,) int64
|
|
162
|
+
Index of the original mixed-face row from which each triangle originates.
|
|
163
|
+
(Triangles coming from the same quad share the same origin id.)
|
|
164
|
+
quad_face_mask : (F,) bool
|
|
165
|
+
Whether each original mixed-face row is a quad (len==4).
|
|
166
|
+
"""
|
|
167
|
+
face_nodes_raw = np.asarray(ds_cur["mesh2d_face_nodes"].values, dtype=np.int64)
|
|
168
|
+
start_index = int(ds_cur["mesh2d_face_nodes"].attrs.get("start_index", 1))
|
|
169
|
+
face_nodes = _face_nodes_raw_to_0b(face_nodes_raw, start_index)
|
|
170
|
+
faces = _faces_from_face_nodes(face_nodes)
|
|
171
|
+
|
|
172
|
+
node_x = np.asarray(ds_cur["mesh2d_node_x"].values, dtype=np.float64)
|
|
173
|
+
node_y = np.asarray(ds_cur["mesh2d_node_y"].values, dtype=np.float64)
|
|
174
|
+
vert_xy = np.column_stack([node_x, node_y])
|
|
175
|
+
xy = vert_xy[:, :2]
|
|
176
|
+
|
|
177
|
+
from ..geomesh_util.grd_util import triangulate_mixed_face_row_to_tris
|
|
178
|
+
|
|
179
|
+
quad_face_mask = np.asarray([(f.size == 4) for f in faces], dtype=bool)
|
|
180
|
+
tris: List[Tuple[int, int, int]] = []
|
|
181
|
+
tri_origin_face_id: List[int] = []
|
|
182
|
+
|
|
183
|
+
for fid, nodes in enumerate(faces):
|
|
184
|
+
tri_list = triangulate_mixed_face_row_to_tris(
|
|
185
|
+
xy, np.asarray(nodes, dtype=np.int64)
|
|
186
|
+
)
|
|
187
|
+
for tri in tri_list:
|
|
188
|
+
tris.append((int(tri[0]), int(tri[1]), int(tri[2])))
|
|
189
|
+
tri_origin_face_id.append(int(fid))
|
|
190
|
+
|
|
191
|
+
tria = np.asarray(tris, dtype=np.int64)
|
|
192
|
+
tri_origin_face_id = np.asarray(tri_origin_face_id, dtype=np.int64)
|
|
193
|
+
|
|
194
|
+
return vert_xy, tria, tri_origin_face_id, quad_face_mask
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def dual_criteria_on_fan_mesh(
|
|
198
|
+
vert: np.ndarray,
|
|
199
|
+
tria: np.ndarray,
|
|
200
|
+
tri_origin_face_id: np.ndarray,
|
|
201
|
+
quad_face_mask: np.ndarray,
|
|
202
|
+
*,
|
|
203
|
+
cosphi_threshold: float,
|
|
204
|
+
removesmalllinkstrsh: float,
|
|
205
|
+
jsferic: int = 1,
|
|
206
|
+
) -> Tuple[bool, float, int]:
|
|
207
|
+
"""Check dual-mesh orthogonality and small-link criteria on a triangle proxy.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
vert : ndarray of shape (N, 2)
|
|
212
|
+
Node coordinates (lon/lat degrees or planar x/y per ``jsferic``).
|
|
213
|
+
tria : ndarray of shape (T, 3)
|
|
214
|
+
0-based triangle connectivity.
|
|
215
|
+
tri_origin_face_id : ndarray of shape (T,)
|
|
216
|
+
Origin mixed-face index for each proxy triangle.
|
|
217
|
+
quad_face_mask : ndarray of shape (F,)
|
|
218
|
+
Whether each original mixed face is a quad.
|
|
219
|
+
cosphi_threshold : float
|
|
220
|
+
Maximum allowed ``|cos φ|`` on internal flow links.
|
|
221
|
+
removesmalllinkstrsh : float
|
|
222
|
+
Small flow-link threshold (Delft3D convention).
|
|
223
|
+
jsferic : int, optional
|
|
224
|
+
``1`` for spherical lon/lat; ``0`` for planar coordinates.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
ok : bool
|
|
229
|
+
``True`` when both criteria are satisfied.
|
|
230
|
+
max_c : float
|
|
231
|
+
Maximum ``|cos φ|`` over counted edges.
|
|
232
|
+
n_small : int
|
|
233
|
+
Number of small flow links detected.
|
|
234
|
+
"""
|
|
235
|
+
from . import orthogonalize as ortho
|
|
236
|
+
from .geometry import build_edges_from_tria
|
|
237
|
+
|
|
238
|
+
vert = np.asarray(vert, dtype=np.float64)
|
|
239
|
+
tria = np.asarray(tria, dtype=np.int64)
|
|
240
|
+
if tria.size == 0:
|
|
241
|
+
return True, 0.0, 0
|
|
242
|
+
|
|
243
|
+
edge_nodes, edge_faces = build_edges_from_tria(tria)
|
|
244
|
+
_, _, cosphi_abs = ortho.compute_cosphi_abs_from_arrays(
|
|
245
|
+
vert[:, 0],
|
|
246
|
+
vert[:, 1],
|
|
247
|
+
tria,
|
|
248
|
+
edge_nodes,
|
|
249
|
+
edge_faces,
|
|
250
|
+
use_file_centers=False,
|
|
251
|
+
use_circumcenter_3d=True,
|
|
252
|
+
jsferic=jsferic,
|
|
253
|
+
)
|
|
254
|
+
# MeshKernel small-flow-links should ignore edges internal to a quad in the
|
|
255
|
+
# original mixed mesh. In the triangle-proxy, those correspond to the shared
|
|
256
|
+
# diagonal between the two triangles coming from the same quad-face row.
|
|
257
|
+
# Apply the same exclusion to max|cos φ|: internal diagonals are not flow
|
|
258
|
+
# links, so they must not force recovery when merge introduces quads.
|
|
259
|
+
n_edges = edge_faces.shape[0]
|
|
260
|
+
keep_edge_indices = np.arange(n_edges, dtype=np.int64)
|
|
261
|
+
exclude_mask = np.zeros(n_edges, dtype=bool)
|
|
262
|
+
for e in range(n_edges):
|
|
263
|
+
f1 = int(edge_faces[e, 0])
|
|
264
|
+
f2 = int(edge_faces[e, 1])
|
|
265
|
+
if f1 < 0 or f2 < 0 or f1 == f2:
|
|
266
|
+
continue
|
|
267
|
+
o1 = int(tri_origin_face_id[f1])
|
|
268
|
+
o2 = int(tri_origin_face_id[f2])
|
|
269
|
+
if o1 == o2 and bool(quad_face_mask[o1]):
|
|
270
|
+
exclude_mask[e] = True
|
|
271
|
+
|
|
272
|
+
mask = ~np.isnan(cosphi_abs) & ~exclude_mask
|
|
273
|
+
max_c = float(np.nanmax(cosphi_abs[mask])) if np.any(mask) else 0.0
|
|
274
|
+
|
|
275
|
+
keep_edge_indices = keep_edge_indices[~exclude_mask]
|
|
276
|
+
n_small, _ = ortho.compute_small_links_from_arrays(
|
|
277
|
+
vert[:, 0],
|
|
278
|
+
vert[:, 1],
|
|
279
|
+
tria,
|
|
280
|
+
edge_nodes,
|
|
281
|
+
edge_faces,
|
|
282
|
+
removesmalllinkstrsh=float(removesmalllinkstrsh),
|
|
283
|
+
edge_indices=keep_edge_indices,
|
|
284
|
+
jsferic=jsferic,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
ok = (max_c <= float(cosphi_threshold) + 1.0e-9) and (int(n_small) == 0)
|
|
288
|
+
return ok, max_c, int(n_small)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def ortho_merge_iterate_dataset(
|
|
292
|
+
ds,
|
|
293
|
+
*,
|
|
294
|
+
outer_iter_max: int = 5,
|
|
295
|
+
cosphi_threshold: float = 0.49,
|
|
296
|
+
removesmalllinkstrsh: float = 0.11,
|
|
297
|
+
buffer_layers: int = 2,
|
|
298
|
+
max_global_iter: int = 6,
|
|
299
|
+
smooth_iter: int = 16,
|
|
300
|
+
enable_edge_flips: bool = True,
|
|
301
|
+
stop_if_no_merge: bool = True,
|
|
302
|
+
ortho_disable_smalllink_logic: bool = True,
|
|
303
|
+
require_both_criteria: bool = False,
|
|
304
|
+
max_recovery_iterations: int = 25,
|
|
305
|
+
recovery_stagnation_break: int = 3,
|
|
306
|
+
outer_stagnation_break: int = 2,
|
|
307
|
+
adaptive_recovery: bool = True,
|
|
308
|
+
recovery_buffer_growth: int = 1,
|
|
309
|
+
recovery_smooth_iter_growth: int = 6,
|
|
310
|
+
recovery_global_iter_growth: int = 1,
|
|
311
|
+
on_state: Optional[Callable[[OrthoMergeStats], None]] = None,
|
|
312
|
+
verbose: bool = True,
|
|
313
|
+
jsferic: int = 1,
|
|
314
|
+
merge_small_links: bool = True,
|
|
315
|
+
fixed: Optional[np.ndarray] = None,
|
|
316
|
+
) -> tuple:
|
|
317
|
+
"""Iteratively orthogonalize and merge circumcenters on a UGRID dataset.
|
|
318
|
+
|
|
319
|
+
Parameters
|
|
320
|
+
----------
|
|
321
|
+
ds : xarray.Dataset
|
|
322
|
+
Delft3D-FM UGRID mesh dataset.
|
|
323
|
+
outer_iter_max : int, optional
|
|
324
|
+
Number of outer ortho+merge cycles.
|
|
325
|
+
cosphi_threshold : float, optional
|
|
326
|
+
Maximum allowed ``|cos φ|`` passed to :func:`orthogonalize_tria_mesh`.
|
|
327
|
+
removesmalllinkstrsh : float, optional
|
|
328
|
+
Small flow-link threshold.
|
|
329
|
+
buffer_layers : int, optional
|
|
330
|
+
Zone buffer depth for orthogonalization.
|
|
331
|
+
max_global_iter : int, optional
|
|
332
|
+
Global orthogonalization passes per cycle.
|
|
333
|
+
smooth_iter : int, optional
|
|
334
|
+
Smoothing iterations per zone pass.
|
|
335
|
+
enable_edge_flips : bool, optional
|
|
336
|
+
Allow quality-guarded edge flips during orthogonalization.
|
|
337
|
+
stop_if_no_merge : bool, optional
|
|
338
|
+
Stop when a merge cycle reduces no faces.
|
|
339
|
+
ortho_disable_smalllink_logic : bool, optional
|
|
340
|
+
If ``True``, delegate small-link removal to merge (not ortho).
|
|
341
|
+
require_both_criteria : bool, optional
|
|
342
|
+
If ``True``, require dual criteria after main and recovery cycles.
|
|
343
|
+
max_recovery_iterations : int, optional
|
|
344
|
+
Extra recovery cycles when criteria fail.
|
|
345
|
+
recovery_stagnation_break : int, optional
|
|
346
|
+
Stop recovery after this many stagnant cycles (0 = disabled).
|
|
347
|
+
outer_stagnation_break : int, optional
|
|
348
|
+
Stop main loop after this many stagnant outer cycles.
|
|
349
|
+
adaptive_recovery : bool, optional
|
|
350
|
+
Increase recovery effort when criteria remain unmet.
|
|
351
|
+
recovery_buffer_growth, recovery_smooth_iter_growth, recovery_global_iter_growth : int, optional
|
|
352
|
+
Per-recovery growth of buffer/smooth/global iteration counts.
|
|
353
|
+
on_state : callable, optional
|
|
354
|
+
Called with :class:`OrthoMergeStats` after each cycle.
|
|
355
|
+
verbose : bool, optional
|
|
356
|
+
Enable per-zone orthogonalization logs.
|
|
357
|
+
jsferic : int, optional
|
|
358
|
+
``1`` for spherical lon/lat; ``0`` for planar coordinates.
|
|
359
|
+
merge_small_links : bool, optional
|
|
360
|
+
If ``False``, keep pure triangles (no quad merge step).
|
|
361
|
+
fixed : array_like of int, optional
|
|
362
|
+
Node indices to hold fixed (never displaced). Node numbering is
|
|
363
|
+
preserved by every pipeline step, so the indices stay valid.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
ds_final : xarray.Dataset
|
|
368
|
+
Updated mesh dataset.
|
|
369
|
+
stats : list of OrthoMergeStats
|
|
370
|
+
Per-cycle statistics.
|
|
371
|
+
"""
|
|
372
|
+
import xarray as xr
|
|
373
|
+
|
|
374
|
+
from ..geomesh_util.merge_circumcenters import (
|
|
375
|
+
_rebuild_ds_from_form,
|
|
376
|
+
build_ugrid_arrays_mixed,
|
|
377
|
+
merge_circumcenters,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
from .orthogonalize import orthogonalize_tria_mesh
|
|
381
|
+
|
|
382
|
+
if not isinstance(ds, xr.Dataset):
|
|
383
|
+
raise TypeError("ds must be an xarray.Dataset")
|
|
384
|
+
|
|
385
|
+
stats: List[OrthoMergeStats] = []
|
|
386
|
+
|
|
387
|
+
# Work on a copy to avoid mutating caller data
|
|
388
|
+
ds_cur = ds.copy(deep=True)
|
|
389
|
+
|
|
390
|
+
def _run_ortho_merge_cycle(
|
|
391
|
+
ds_in,
|
|
392
|
+
*,
|
|
393
|
+
buffer_layers_override: Optional[int] = None,
|
|
394
|
+
max_global_iter_override: Optional[int] = None,
|
|
395
|
+
smooth_iter_override: Optional[int] = None,
|
|
396
|
+
):
|
|
397
|
+
"""One ortho (dual-consistent tris of mixed faces) + merge_circumcenters. Returns updated ds."""
|
|
398
|
+
ds_before_outer = ds_in.copy(deep=True)
|
|
399
|
+
node_x = np.asarray(ds_in["mesh2d_node_x"].values, dtype=np.float64)
|
|
400
|
+
node_y = np.asarray(ds_in["mesh2d_node_y"].values, dtype=np.float64)
|
|
401
|
+
node_z = (
|
|
402
|
+
np.asarray(ds_in["mesh2d_node_z"].values, dtype=np.float64)
|
|
403
|
+
if "mesh2d_node_z" in ds_in
|
|
404
|
+
else np.zeros((node_x.shape[0],), dtype=np.float64)
|
|
405
|
+
)
|
|
406
|
+
vert = np.column_stack([node_x, node_y])
|
|
407
|
+
face_nodes_raw = np.asarray(ds_in["mesh2d_face_nodes"].values, dtype=np.int64)
|
|
408
|
+
start_index = int(ds_in["mesh2d_face_nodes"].attrs.get("start_index", 1))
|
|
409
|
+
face_nodes = _face_nodes_raw_to_0b(face_nodes_raw, start_index)
|
|
410
|
+
faces = _faces_from_face_nodes(face_nodes)
|
|
411
|
+
tria_for_ortho, tri_face_id = _triangulate_faces_for_ortho(vert, faces)
|
|
412
|
+
|
|
413
|
+
bl = int(
|
|
414
|
+
buffer_layers if buffer_layers_override is None else buffer_layers_override
|
|
415
|
+
)
|
|
416
|
+
mgi = int(
|
|
417
|
+
max_global_iter
|
|
418
|
+
if max_global_iter_override is None
|
|
419
|
+
else max_global_iter_override
|
|
420
|
+
)
|
|
421
|
+
si = int(smooth_iter if smooth_iter_override is None else smooth_iter_override)
|
|
422
|
+
|
|
423
|
+
if merge_small_links:
|
|
424
|
+
# Small links are removed by merge_circumcenters below; keep the
|
|
425
|
+
# orthogonalizer focused on |cosphi| only.
|
|
426
|
+
ortho_smalllink_trsh = (
|
|
427
|
+
1.0e-12 if ortho_disable_smalllink_logic else removesmalllinkstrsh
|
|
428
|
+
)
|
|
429
|
+
else:
|
|
430
|
+
# Triangles-only mode: the orthogonalizer itself must clear small
|
|
431
|
+
# links (guarded flips + circumcenter-separation displacement).
|
|
432
|
+
ortho_smalllink_trsh = removesmalllinkstrsh
|
|
433
|
+
ortho_res = orthogonalize_tria_mesh(
|
|
434
|
+
vert,
|
|
435
|
+
tria_for_ortho,
|
|
436
|
+
cosphi_threshold=cosphi_threshold,
|
|
437
|
+
removesmalllinkstrsh=ortho_smalllink_trsh,
|
|
438
|
+
buffer_layers=bl,
|
|
439
|
+
max_global_iter=mgi,
|
|
440
|
+
smooth_iter=si,
|
|
441
|
+
enable_edge_flips=enable_edge_flips,
|
|
442
|
+
verbose=verbose,
|
|
443
|
+
jsferic=jsferic,
|
|
444
|
+
smalllink_priority=not merge_small_links,
|
|
445
|
+
fixed=fixed,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
NODE = np.column_stack([ortho_res.vert[:, 0], ortho_res.vert[:, 1], node_z])
|
|
449
|
+
faces_after_ortho = _faces_with_ortho_topology(
|
|
450
|
+
faces,
|
|
451
|
+
tria_for_ortho,
|
|
452
|
+
np.asarray(ortho_res.tria, dtype=np.int64),
|
|
453
|
+
tri_face_id,
|
|
454
|
+
)
|
|
455
|
+
ugrid_arrays = build_ugrid_arrays_mixed(NODE, faces_after_ortho)
|
|
456
|
+
ds_after_ortho = _rebuild_ds_from_form(ds_in, ugrid_arrays)
|
|
457
|
+
|
|
458
|
+
if not merge_small_links:
|
|
459
|
+
return ds_after_ortho, ortho_res, 0, ds_before_outer
|
|
460
|
+
|
|
461
|
+
nfaces_before = int(
|
|
462
|
+
ds_after_ortho.sizes.get(
|
|
463
|
+
"mesh2d_nFaces", ds_after_ortho["mesh2d_face_nodes"].shape[0]
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
ds_merged = merge_circumcenters(
|
|
467
|
+
ds_after_ortho, removesmalllinkstrsh=removesmalllinkstrsh, jsferic=jsferic
|
|
468
|
+
)
|
|
469
|
+
nfaces_after = int(
|
|
470
|
+
ds_merged.sizes.get(
|
|
471
|
+
"mesh2d_nFaces", ds_merged["mesh2d_face_nodes"].shape[0]
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
merged_this_iter = max(0, nfaces_before - nfaces_after)
|
|
475
|
+
|
|
476
|
+
return ds_merged, ortho_res, merged_this_iter, ds_before_outer
|
|
477
|
+
|
|
478
|
+
outer_stall_limit = max(0, int(outer_stagnation_break))
|
|
479
|
+
outer_stall_count = 0
|
|
480
|
+
for outer in range(int(outer_iter_max)):
|
|
481
|
+
ds_cur, ortho_res, merged_this_iter, ds_before_outer = _run_ortho_merge_cycle(
|
|
482
|
+
ds_cur
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Early stop on stagnation: if the same outer-cycle metrics repeat,
|
|
486
|
+
# further global passes are unlikely to help and are expensive.
|
|
487
|
+
if len(stats) > 0:
|
|
488
|
+
prev = stats[-1]
|
|
489
|
+
same_metric = (
|
|
490
|
+
abs(float(ortho_res.max_cosphi) - float(prev.max_cosphi)) <= 1.0e-9
|
|
491
|
+
and int(ortho_res.n_small_flow_links) == int(prev.n_small_flow_links)
|
|
492
|
+
and int(merged_this_iter) == int(prev.merged_this_iter)
|
|
493
|
+
)
|
|
494
|
+
if same_metric:
|
|
495
|
+
outer_stall_count += 1
|
|
496
|
+
if outer_stall_limit > 0 and outer_stall_count >= outer_stall_limit:
|
|
497
|
+
stats.append(
|
|
498
|
+
OrthoMergeStats(
|
|
499
|
+
outer_iter=outer,
|
|
500
|
+
max_cosphi=float(ortho_res.max_cosphi),
|
|
501
|
+
n_small_flow_links=int(ortho_res.n_small_flow_links),
|
|
502
|
+
merged_this_iter=int(merged_this_iter),
|
|
503
|
+
n_zones_orthogonalized=int(
|
|
504
|
+
getattr(ortho_res, "n_zones_orthogonalized", 0)
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
if on_state is not None:
|
|
509
|
+
on_state(stats[-1])
|
|
510
|
+
break
|
|
511
|
+
else:
|
|
512
|
+
outer_stall_count = 0
|
|
513
|
+
|
|
514
|
+
# Guardrail: if no merge happened and orthogonality got worse than previous outer-iter,
|
|
515
|
+
# revert this outer step and stop.
|
|
516
|
+
if (
|
|
517
|
+
merged_this_iter == 0
|
|
518
|
+
and len(stats) > 0
|
|
519
|
+
and float(ortho_res.max_cosphi) > (float(stats[-1].max_cosphi) + 1.0e-9)
|
|
520
|
+
):
|
|
521
|
+
ds_cur = ds_before_outer
|
|
522
|
+
stats.append(
|
|
523
|
+
OrthoMergeStats(
|
|
524
|
+
outer_iter=outer,
|
|
525
|
+
max_cosphi=float(stats[-1].max_cosphi),
|
|
526
|
+
n_small_flow_links=int(stats[-1].n_small_flow_links),
|
|
527
|
+
merged_this_iter=0,
|
|
528
|
+
n_zones_orthogonalized=int(
|
|
529
|
+
getattr(ortho_res, "n_zones_orthogonalized", 0)
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
if on_state is not None:
|
|
534
|
+
on_state(stats[-1])
|
|
535
|
+
break
|
|
536
|
+
stats.append(
|
|
537
|
+
OrthoMergeStats(
|
|
538
|
+
outer_iter=outer,
|
|
539
|
+
max_cosphi=float(ortho_res.max_cosphi),
|
|
540
|
+
n_small_flow_links=int(ortho_res.n_small_flow_links),
|
|
541
|
+
merged_this_iter=int(merged_this_iter),
|
|
542
|
+
n_zones_orthogonalized=int(
|
|
543
|
+
getattr(ortho_res, "n_zones_orthogonalized", 0)
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
if on_state is not None:
|
|
548
|
+
on_state(stats[-1])
|
|
549
|
+
|
|
550
|
+
# Not meaningful in triangles-only mode (nothing is ever merged).
|
|
551
|
+
if merge_small_links and stop_if_no_merge and merged_this_iter == 0:
|
|
552
|
+
break
|
|
553
|
+
|
|
554
|
+
if bool(require_both_criteria):
|
|
555
|
+
v_chk, t_chk, tri_origin_face_id, quad_face_mask = _fan_vert_tria_from_ds(
|
|
556
|
+
ds_cur
|
|
557
|
+
)
|
|
558
|
+
ok, max_c, n_s = dual_criteria_on_fan_mesh(
|
|
559
|
+
v_chk,
|
|
560
|
+
t_chk,
|
|
561
|
+
tri_origin_face_id,
|
|
562
|
+
quad_face_mask,
|
|
563
|
+
cosphi_threshold=cosphi_threshold,
|
|
564
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
565
|
+
jsferic=jsferic,
|
|
566
|
+
)
|
|
567
|
+
max_rec = max(0, int(max_recovery_iterations))
|
|
568
|
+
stall_need = int(recovery_stagnation_break)
|
|
569
|
+
prev_metric_key = (round(max_c, 9), int(n_s))
|
|
570
|
+
stall = 0
|
|
571
|
+
r = 0
|
|
572
|
+
while (not ok) and r < max_rec:
|
|
573
|
+
if bool(adaptive_recovery):
|
|
574
|
+
growth_steps = 1 + r + stall
|
|
575
|
+
bl_rec = int(
|
|
576
|
+
max(1, buffer_layers + int(recovery_buffer_growth) * growth_steps)
|
|
577
|
+
)
|
|
578
|
+
mgi_rec = int(
|
|
579
|
+
max(
|
|
580
|
+
1,
|
|
581
|
+
max_global_iter
|
|
582
|
+
+ int(recovery_global_iter_growth) * growth_steps,
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
si_rec = int(
|
|
586
|
+
max(
|
|
587
|
+
1, smooth_iter + int(recovery_smooth_iter_growth) * growth_steps
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
else:
|
|
591
|
+
bl_rec = int(buffer_layers)
|
|
592
|
+
mgi_rec = int(max_global_iter)
|
|
593
|
+
si_rec = int(smooth_iter)
|
|
594
|
+
|
|
595
|
+
ds_cur, ortho_res, merged_this_iter, ds_before_outer = (
|
|
596
|
+
_run_ortho_merge_cycle(
|
|
597
|
+
ds_cur,
|
|
598
|
+
buffer_layers_override=bl_rec,
|
|
599
|
+
max_global_iter_override=mgi_rec,
|
|
600
|
+
smooth_iter_override=si_rec,
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
# Recovery: no guardrail revert (keep trying); always log cycle.
|
|
604
|
+
stats.append(
|
|
605
|
+
OrthoMergeStats(
|
|
606
|
+
outer_iter=r,
|
|
607
|
+
max_cosphi=float(ortho_res.max_cosphi),
|
|
608
|
+
n_small_flow_links=int(ortho_res.n_small_flow_links),
|
|
609
|
+
merged_this_iter=int(merged_this_iter),
|
|
610
|
+
n_zones_orthogonalized=int(
|
|
611
|
+
getattr(ortho_res, "n_zones_orthogonalized", 0)
|
|
612
|
+
),
|
|
613
|
+
recovery=True,
|
|
614
|
+
recovery_iter=r,
|
|
615
|
+
)
|
|
616
|
+
)
|
|
617
|
+
if on_state is not None:
|
|
618
|
+
on_state(stats[-1])
|
|
619
|
+
v_chk, t_chk, tri_origin_face_id, quad_face_mask = _fan_vert_tria_from_ds(
|
|
620
|
+
ds_cur
|
|
621
|
+
)
|
|
622
|
+
ok, max_c, n_s = dual_criteria_on_fan_mesh(
|
|
623
|
+
v_chk,
|
|
624
|
+
t_chk,
|
|
625
|
+
tri_origin_face_id,
|
|
626
|
+
quad_face_mask,
|
|
627
|
+
cosphi_threshold=cosphi_threshold,
|
|
628
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
629
|
+
jsferic=jsferic,
|
|
630
|
+
)
|
|
631
|
+
r += 1
|
|
632
|
+
if stall_need > 0:
|
|
633
|
+
key = (round(max_c, 9), int(n_s))
|
|
634
|
+
if key == prev_metric_key:
|
|
635
|
+
stall += 1
|
|
636
|
+
if stall >= stall_need:
|
|
637
|
+
break
|
|
638
|
+
else:
|
|
639
|
+
stall = 0
|
|
640
|
+
prev_metric_key = key
|
|
641
|
+
|
|
642
|
+
if not ok:
|
|
643
|
+
raise RuntimeError(
|
|
644
|
+
"ortho_merge_iterate_dataset: mesh still violates dual criteria after "
|
|
645
|
+
f"{int(outer_iter_max)} main cycle(s) and {r} recovery cycle(s). "
|
|
646
|
+
f"Required: max|cosφ| <= {cosphi_threshold} and n_small_flow_links == 0 "
|
|
647
|
+
f"(triangle proxy). Got max|cosφ|={max_c:.6f}, n_small={n_s}. "
|
|
648
|
+
"Increase max_recovery_iterations / outer_iter_max, relax thresholds, "
|
|
649
|
+
"or improve the initial mesh."
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return ds_cur, stats
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def print_stats(
|
|
656
|
+
stats: Sequence[OrthoMergeStats], *, print_header: bool = False
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Print a compact ortho+merge progress table.
|
|
659
|
+
|
|
660
|
+
Parameters
|
|
661
|
+
----------
|
|
662
|
+
stats : sequence of OrthoMergeStats
|
|
663
|
+
Statistics rows to display.
|
|
664
|
+
print_header : bool, optional
|
|
665
|
+
If ``True``, print the column header first.
|
|
666
|
+
"""
|
|
667
|
+
if print_header:
|
|
668
|
+
print(" -------------------------------------------------------")
|
|
669
|
+
print(" |STATE.| |MAX|COS(PHI)| |N_SMALL| |N_ZONES|")
|
|
670
|
+
print(" -------------------------------------------------------")
|
|
671
|
+
|
|
672
|
+
for s in stats:
|
|
673
|
+
if getattr(s, "recovery", False):
|
|
674
|
+
head = f"recovery={s.recovery_iter}"
|
|
675
|
+
else:
|
|
676
|
+
# Special markers used by smood for start/end snapshots.
|
|
677
|
+
if int(s.outer_iter) == -1:
|
|
678
|
+
head = "initial"
|
|
679
|
+
elif int(s.outer_iter) == -2:
|
|
680
|
+
head = "final"
|
|
681
|
+
else:
|
|
682
|
+
head = f"outer={s.outer_iter}"
|
|
683
|
+
|
|
684
|
+
print(
|
|
685
|
+
f" {head:<11}{s.max_cosphi:>13.6f}{int(s.n_small_flow_links):>14d}{int(s.n_zones_orthogonalized):>11d}",
|
|
686
|
+
flush=True,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def ortho_merge_iterate_tria(
|
|
691
|
+
vert: np.ndarray,
|
|
692
|
+
tria: np.ndarray,
|
|
693
|
+
*,
|
|
694
|
+
node_z: Optional[np.ndarray] = None,
|
|
695
|
+
outer_iter_max: int = 5,
|
|
696
|
+
cosphi_threshold: float = 0.49,
|
|
697
|
+
removesmalllinkstrsh: float = 0.11,
|
|
698
|
+
buffer_layers: int = 2,
|
|
699
|
+
max_global_iter: int = 6,
|
|
700
|
+
smooth_iter: int = 16,
|
|
701
|
+
enable_edge_flips: bool = True,
|
|
702
|
+
stop_if_no_merge: bool = True,
|
|
703
|
+
ortho_disable_smalllink_logic: bool = True,
|
|
704
|
+
require_both_criteria: bool = False,
|
|
705
|
+
max_recovery_iterations: int = 25,
|
|
706
|
+
recovery_stagnation_break: int = 3,
|
|
707
|
+
outer_stagnation_break: int = 2,
|
|
708
|
+
adaptive_recovery: bool = True,
|
|
709
|
+
recovery_buffer_growth: int = 1,
|
|
710
|
+
recovery_smooth_iter_growth: int = 6,
|
|
711
|
+
recovery_global_iter_growth: int = 1,
|
|
712
|
+
on_state: Optional[Callable[[OrthoMergeStats], None]] = None,
|
|
713
|
+
verbose: bool = True,
|
|
714
|
+
jsferic: int = 1,
|
|
715
|
+
merge_small_links: bool = True,
|
|
716
|
+
fixed: Optional[np.ndarray] = None,
|
|
717
|
+
) -> tuple:
|
|
718
|
+
"""Run the ortho+merge pipeline starting from a pure triangle mesh.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
vert : ndarray of shape (N, 2)
|
|
723
|
+
Node coordinates (lon/lat degrees or planar x/y per ``jsferic``).
|
|
724
|
+
tria : ndarray of shape (T, 3)
|
|
725
|
+
0-based triangle connectivity.
|
|
726
|
+
node_z : ndarray of shape (N,), optional
|
|
727
|
+
Node elevations; zeros used if ``None``.
|
|
728
|
+
outer_iter_max, cosphi_threshold, removesmalllinkstrsh, buffer_layers,
|
|
729
|
+
max_global_iter, smooth_iter, enable_edge_flips, stop_if_no_merge,
|
|
730
|
+
ortho_disable_smalllink_logic, require_both_criteria,
|
|
731
|
+
max_recovery_iterations, recovery_stagnation_break, outer_stagnation_break,
|
|
732
|
+
adaptive_recovery, recovery_buffer_growth, recovery_smooth_iter_growth,
|
|
733
|
+
recovery_global_iter_growth, on_state, verbose, jsferic, merge_small_links
|
|
734
|
+
Same meaning as in :func:`ortho_merge_iterate_dataset`.
|
|
735
|
+
|
|
736
|
+
Returns
|
|
737
|
+
-------
|
|
738
|
+
vert_out : ndarray of shape (N, 2)
|
|
739
|
+
Updated node coordinates.
|
|
740
|
+
face_nodes_0b : ndarray of shape (F, 4)
|
|
741
|
+
Mixed face-node rows (0-based, ``-1`` fill for triangles).
|
|
742
|
+
stats : list of OrthoMergeStats
|
|
743
|
+
Per-cycle statistics.
|
|
744
|
+
"""
|
|
745
|
+
import xarray as xr
|
|
746
|
+
|
|
747
|
+
from ..geomesh_util.grd_util import adcirc2DFlowFM
|
|
748
|
+
|
|
749
|
+
vert = np.asarray(vert, dtype=np.float64)
|
|
750
|
+
tria = np.asarray(tria, dtype=np.int64)
|
|
751
|
+
if vert.ndim != 2 or vert.shape[1] != 2:
|
|
752
|
+
raise ValueError("vert must have shape (N,2)")
|
|
753
|
+
if tria.ndim != 2 or tria.shape[1] != 3:
|
|
754
|
+
raise ValueError("tria must have shape (T,3)")
|
|
755
|
+
|
|
756
|
+
if node_z is None:
|
|
757
|
+
node_z = np.zeros((vert.shape[0],), dtype=np.float64)
|
|
758
|
+
else:
|
|
759
|
+
node_z = np.asarray(node_z, dtype=np.float64).reshape(-1)
|
|
760
|
+
if node_z.shape[0] != vert.shape[0]:
|
|
761
|
+
raise ValueError("node_z must have length N (same as vert)")
|
|
762
|
+
|
|
763
|
+
NODE = np.column_stack([vert[:, 0], vert[:, 1], node_z])
|
|
764
|
+
ds0 = adcirc2DFlowFM(NODE=NODE, EDGE=tria)
|
|
765
|
+
if not isinstance(ds0, xr.Dataset):
|
|
766
|
+
raise TypeError(
|
|
767
|
+
"adcirc2DFlowFM must return an xarray.Dataset in this repository"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
ds_final, stats = ortho_merge_iterate_dataset(
|
|
771
|
+
ds0,
|
|
772
|
+
outer_iter_max=outer_iter_max,
|
|
773
|
+
cosphi_threshold=cosphi_threshold,
|
|
774
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
775
|
+
buffer_layers=buffer_layers,
|
|
776
|
+
max_global_iter=max_global_iter,
|
|
777
|
+
smooth_iter=smooth_iter,
|
|
778
|
+
enable_edge_flips=enable_edge_flips,
|
|
779
|
+
stop_if_no_merge=stop_if_no_merge,
|
|
780
|
+
ortho_disable_smalllink_logic=ortho_disable_smalllink_logic,
|
|
781
|
+
require_both_criteria=require_both_criteria,
|
|
782
|
+
max_recovery_iterations=max_recovery_iterations,
|
|
783
|
+
recovery_stagnation_break=recovery_stagnation_break,
|
|
784
|
+
outer_stagnation_break=outer_stagnation_break,
|
|
785
|
+
adaptive_recovery=adaptive_recovery,
|
|
786
|
+
recovery_buffer_growth=recovery_buffer_growth,
|
|
787
|
+
recovery_smooth_iter_growth=recovery_smooth_iter_growth,
|
|
788
|
+
recovery_global_iter_growth=recovery_global_iter_growth,
|
|
789
|
+
on_state=on_state,
|
|
790
|
+
verbose=verbose,
|
|
791
|
+
jsferic=jsferic,
|
|
792
|
+
merge_small_links=merge_small_links,
|
|
793
|
+
fixed=fixed,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
vert_out = np.column_stack(
|
|
797
|
+
[
|
|
798
|
+
np.asarray(ds_final["mesh2d_node_x"].values, dtype=np.float64),
|
|
799
|
+
np.asarray(ds_final["mesh2d_node_y"].values, dtype=np.float64),
|
|
800
|
+
]
|
|
801
|
+
)
|
|
802
|
+
face_nodes_1b = np.asarray(ds_final["mesh2d_face_nodes"].values, dtype=np.int64)
|
|
803
|
+
start_index = int(ds_final["mesh2d_face_nodes"].attrs.get("start_index", 1))
|
|
804
|
+
fill = -1
|
|
805
|
+
if start_index == 1:
|
|
806
|
+
# Common convention: invalid nodes are stored as 0 for 1-based meshes.
|
|
807
|
+
face_nodes_0b = np.where(face_nodes_1b > 0, face_nodes_1b - 1, fill)
|
|
808
|
+
else:
|
|
809
|
+
# Assume already 0-based with invalid encoded as -1.
|
|
810
|
+
face_nodes_0b = face_nodes_1b.copy()
|
|
811
|
+
|
|
812
|
+
return vert_out, face_nodes_0b, stats
|