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
bluemesh2d/smood.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .geomesh_util.grd_util import triangulate_mixed_face_row_to_tris
|
|
3
|
+
from .mesh_util.tricon import tricon
|
|
4
|
+
from .ortho_merge.constants import DEFAULT_SMALLLINK_THRESHOLD
|
|
5
|
+
|
|
6
|
+
DEFAULT_REQUIRE_STRICT_DUAL: bool = True
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _ortho_merge_pipeline(vert, conn, tria, tnum, opts, fixed=None):
|
|
10
|
+
"""Run repeated orthogonalize and merge_circumcenters cycles on a triangle mesh."""
|
|
11
|
+
from .ortho_merge.ortho_merge_iter import ortho_merge_iterate_tria, print_stats
|
|
12
|
+
|
|
13
|
+
vert_in = np.asarray(vert, dtype=np.float64)
|
|
14
|
+
tria_in = np.asarray(tria, dtype=np.int64)
|
|
15
|
+
tnum_in = np.asarray(tnum, dtype=np.int64).reshape(-1)
|
|
16
|
+
|
|
17
|
+
outer_iter_max = max(1, int(opts.get("iter", 4)))
|
|
18
|
+
|
|
19
|
+
smalllink_trsh = float(opts.get("smalllink_threshold", DEFAULT_SMALLLINK_THRESHOLD))
|
|
20
|
+
require_strict = bool(
|
|
21
|
+
opts.get("require_both_criteria", DEFAULT_REQUIRE_STRICT_DUAL)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# jsferic=1 -> spherical (lon/lat degrees); jsferic=0 -> planar x/y.
|
|
25
|
+
jsferic = 1 if bool(opts.get("spherical", False)) else 0
|
|
26
|
+
|
|
27
|
+
# Triangles-only mode: clear small flow links by guarded flips and node
|
|
28
|
+
# movement instead of merging triangle pairs into quads.
|
|
29
|
+
merge_small_links = bool(opts.get("merge_small_links", False))
|
|
30
|
+
|
|
31
|
+
do_log = not np.isinf(opts.get("disp", 4))
|
|
32
|
+
|
|
33
|
+
# Initial snapshot (triangle proxy: 1 mixed-face row per input triangle).
|
|
34
|
+
init_max_c = None
|
|
35
|
+
init_n_small = None
|
|
36
|
+
if do_log:
|
|
37
|
+
cosphi_threshold = float(opts.get("orthogonality_threshold", 0.49))
|
|
38
|
+
removesmalllinkstrsh = smalllink_trsh
|
|
39
|
+
tri_origin_face_id = np.arange(tria_in.shape[0], dtype=np.int64)
|
|
40
|
+
quad_face_mask = np.zeros(tria_in.shape[0], dtype=bool)
|
|
41
|
+
|
|
42
|
+
from .ortho_merge.ortho_merge_iter import (
|
|
43
|
+
dual_criteria_on_fan_mesh,
|
|
44
|
+
OrthoMergeStats,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_, init_max_c, init_n_small = dual_criteria_on_fan_mesh(
|
|
48
|
+
np.asarray(vert_in, dtype=np.float64),
|
|
49
|
+
tria_in,
|
|
50
|
+
tri_origin_face_id,
|
|
51
|
+
quad_face_mask,
|
|
52
|
+
cosphi_threshold=cosphi_threshold,
|
|
53
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
54
|
+
jsferic=jsferic,
|
|
55
|
+
)
|
|
56
|
+
print_stats(
|
|
57
|
+
[
|
|
58
|
+
OrthoMergeStats(
|
|
59
|
+
outer_iter=-1,
|
|
60
|
+
max_cosphi=float(init_max_c),
|
|
61
|
+
n_small_flow_links=int(init_n_small),
|
|
62
|
+
merged_this_iter=0,
|
|
63
|
+
n_zones_orthogonalized=0,
|
|
64
|
+
)
|
|
65
|
+
],
|
|
66
|
+
print_header=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _on_state(s):
|
|
70
|
+
if do_log:
|
|
71
|
+
print_stats([s], print_header=False)
|
|
72
|
+
|
|
73
|
+
# Keep output compact: disable verbose per-zone logs inside meshkernel orthogonalization.
|
|
74
|
+
vert_out, face_nodes_0b, stats = ortho_merge_iterate_tria(
|
|
75
|
+
vert_in,
|
|
76
|
+
tria_in,
|
|
77
|
+
node_z=None,
|
|
78
|
+
outer_iter_max=outer_iter_max,
|
|
79
|
+
cosphi_threshold=float(opts.get("orthogonality_threshold", 0.49)),
|
|
80
|
+
removesmalllinkstrsh=smalllink_trsh,
|
|
81
|
+
buffer_layers=int(opts.get("buffer_layers", 2)),
|
|
82
|
+
max_global_iter=int(
|
|
83
|
+
opts.get("max_global_iter", int(opts.get("inner_iter", 4)) + 2)
|
|
84
|
+
),
|
|
85
|
+
smooth_iter=int(opts.get("smooth_iter", int(opts.get("inner_iter", 4)) * 4)),
|
|
86
|
+
enable_edge_flips=bool(opts.get("enable_edge_flips", True)),
|
|
87
|
+
stop_if_no_merge=bool(opts.get("stop_if_no_merge", False)),
|
|
88
|
+
ortho_disable_smalllink_logic=True,
|
|
89
|
+
require_both_criteria=require_strict,
|
|
90
|
+
max_recovery_iterations=int(opts.get("max_recovery_iterations", 100)),
|
|
91
|
+
recovery_stagnation_break=int(opts.get("recovery_stagnation_break", 10)),
|
|
92
|
+
outer_stagnation_break=int(opts.get("outer_stagnation_break", 2)),
|
|
93
|
+
adaptive_recovery=bool(opts.get("adaptive_recovery", True)),
|
|
94
|
+
recovery_buffer_growth=int(opts.get("recovery_buffer_growth", 1)),
|
|
95
|
+
recovery_smooth_iter_growth=int(opts.get("recovery_smooth_iter_growth", 6)),
|
|
96
|
+
recovery_global_iter_growth=int(opts.get("recovery_global_iter_growth", 1)),
|
|
97
|
+
on_state=_on_state if do_log else None,
|
|
98
|
+
verbose=False,
|
|
99
|
+
jsferic=jsferic,
|
|
100
|
+
merge_small_links=merge_small_links,
|
|
101
|
+
fixed=fixed,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Final snapshot (triangle proxy built from mixed faces).
|
|
105
|
+
if do_log:
|
|
106
|
+
cosphi_threshold = float(opts.get("orthogonality_threshold", 0.49))
|
|
107
|
+
removesmalllinkstrsh = smalllink_trsh
|
|
108
|
+
|
|
109
|
+
from .ortho_merge.ortho_merge_iter import (
|
|
110
|
+
dual_criteria_on_fan_mesh,
|
|
111
|
+
OrthoMergeStats,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
face_nodes_0b_arr = np.asarray(face_nodes_0b, dtype=np.int64)
|
|
115
|
+
vert_xy = np.asarray(vert_out, dtype=np.float64)
|
|
116
|
+
|
|
117
|
+
tria_proxy = []
|
|
118
|
+
tri_origin_face_id = []
|
|
119
|
+
quad_face_mask = np.zeros(face_nodes_0b_arr.shape[0], dtype=bool)
|
|
120
|
+
|
|
121
|
+
for fid, row in enumerate(face_nodes_0b_arr):
|
|
122
|
+
nodes = row[row >= 0]
|
|
123
|
+
if nodes.size < 3:
|
|
124
|
+
continue
|
|
125
|
+
quad_face_mask[fid] = nodes.size == 4
|
|
126
|
+
for t in triangulate_mixed_face_row_to_tris(vert_xy, nodes):
|
|
127
|
+
tria_proxy.append(t)
|
|
128
|
+
tri_origin_face_id.append(fid)
|
|
129
|
+
|
|
130
|
+
tria_proxy = np.asarray(tria_proxy, dtype=np.int64)
|
|
131
|
+
tri_origin_face_id = np.asarray(tri_origin_face_id, dtype=np.int64)
|
|
132
|
+
|
|
133
|
+
_, fin_max_c, fin_n_small = dual_criteria_on_fan_mesh(
|
|
134
|
+
vert_xy,
|
|
135
|
+
tria_proxy,
|
|
136
|
+
tri_origin_face_id,
|
|
137
|
+
quad_face_mask,
|
|
138
|
+
cosphi_threshold=cosphi_threshold,
|
|
139
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
140
|
+
jsferic=jsferic,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
last_zones = (
|
|
144
|
+
int(getattr(stats[-1], "n_zones_orthogonalized", 0)) if stats else 0
|
|
145
|
+
)
|
|
146
|
+
print_stats(
|
|
147
|
+
[
|
|
148
|
+
OrthoMergeStats(
|
|
149
|
+
outer_iter=-2,
|
|
150
|
+
max_cosphi=float(fin_max_c),
|
|
151
|
+
n_small_flow_links=int(fin_n_small),
|
|
152
|
+
merged_this_iter=0,
|
|
153
|
+
n_zones_orthogonalized=last_zones,
|
|
154
|
+
)
|
|
155
|
+
],
|
|
156
|
+
print_header=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Optionally keep the merged mixed faces (quads) for UGRID export.
|
|
160
|
+
preserve_merged_quads = bool(opts.get("preserve_merged_quads", False))
|
|
161
|
+
face_nodes_arr = np.asarray(face_nodes_0b, dtype=np.int64)
|
|
162
|
+
|
|
163
|
+
if preserve_merged_quads:
|
|
164
|
+
opts["_mixed_face_nodes_0b"] = face_nodes_arr.copy()
|
|
165
|
+
else:
|
|
166
|
+
opts.pop("_mixed_face_nodes_0b", None)
|
|
167
|
+
|
|
168
|
+
# If we actually have quads and the caller asked to preserve them, return
|
|
169
|
+
# ``face_nodes_0b`` directly as the 3rd output (`tria`), so callers can do:
|
|
170
|
+
# ds_out = adcirc2DFlowFM(NODE, tria)
|
|
171
|
+
# without needing the separate `mixed_fn` conditional.
|
|
172
|
+
#
|
|
173
|
+
# For purely triangulated meshes (no quads), keep the historical return
|
|
174
|
+
# type: triangle connectivity (T,3).
|
|
175
|
+
valid_counts = np.sum(face_nodes_arr >= 0, axis=1)
|
|
176
|
+
has_quads = bool(np.any(valid_counts == 4))
|
|
177
|
+
if preserve_merged_quads and has_quads:
|
|
178
|
+
tria_out = face_nodes_arr.copy()
|
|
179
|
+
# `tnum` is not used by `adcirc2DFlowFM`, but keep the shape consistent
|
|
180
|
+
# with the returned face rows.
|
|
181
|
+
tnum_out = np.ones((tria_out.shape[0], 1), dtype=np.int64)
|
|
182
|
+
return np.asarray(vert_out, dtype=np.float64), conn, tria_out, tnum_out
|
|
183
|
+
|
|
184
|
+
# Triangle-only connectivity.
|
|
185
|
+
# Quads from merge_circumcenters: split on diagonal (v1,v2), not fan-from-a.
|
|
186
|
+
new_tris = []
|
|
187
|
+
new_parts = []
|
|
188
|
+
tri_origin_face_id_export: list = []
|
|
189
|
+
quad_face_mask_export = np.zeros(face_nodes_arr.shape[0], dtype=bool)
|
|
190
|
+
vert_xy = np.asarray(vert_out, dtype=np.float64)
|
|
191
|
+
for fid, row in enumerate(face_nodes_arr):
|
|
192
|
+
nodes = row[row >= 0]
|
|
193
|
+
if nodes.size < 3:
|
|
194
|
+
continue
|
|
195
|
+
quad_face_mask_export[fid] = nodes.size == 4
|
|
196
|
+
for t in triangulate_mixed_face_row_to_tris(vert_xy, nodes):
|
|
197
|
+
new_tris.append(t)
|
|
198
|
+
new_parts.append(1)
|
|
199
|
+
tri_origin_face_id_export.append(int(fid))
|
|
200
|
+
|
|
201
|
+
tria_out = np.asarray(new_tris, dtype=np.int64)
|
|
202
|
+
if tria_out.size == 0:
|
|
203
|
+
tria_out = tria_in.copy()
|
|
204
|
+
tnum_out = np.asarray(tnum, dtype=np.int64)
|
|
205
|
+
tri_origin_face_id_for_dual = np.arange(tria_out.shape[0], dtype=np.int64)
|
|
206
|
+
quad_face_mask_for_dual = np.zeros(tria_out.shape[0], dtype=bool)
|
|
207
|
+
else:
|
|
208
|
+
tnum_out = np.asarray(new_parts, dtype=np.int64).reshape(-1, 1)
|
|
209
|
+
tri_origin_face_id_for_dual = np.asarray(
|
|
210
|
+
tri_origin_face_id_export, dtype=np.int64
|
|
211
|
+
)
|
|
212
|
+
quad_face_mask_for_dual = quad_face_mask_export
|
|
213
|
+
|
|
214
|
+
# Optional post-pass: enforce dual criteria on the final triangle output.
|
|
215
|
+
# This addresses cases where outer-cycle stats are good but exported triangle
|
|
216
|
+
# connectivity still shows degraded max|cos(phi)|.
|
|
217
|
+
enforce_output_dual = bool(
|
|
218
|
+
opts.get("enforce_output_dual_criteria", bool(require_strict))
|
|
219
|
+
)
|
|
220
|
+
if enforce_output_dual and tria_out.size > 0:
|
|
221
|
+
from .ortho_merge.orthogonalize import orthogonalize_tria_mesh
|
|
222
|
+
from .ortho_merge.ortho_merge_iter import dual_criteria_on_fan_mesh
|
|
223
|
+
|
|
224
|
+
post_iter = int(opts.get("post_output_ortho_iter", 3))
|
|
225
|
+
post_iter = max(1, post_iter)
|
|
226
|
+
cosphi_threshold = float(opts.get("orthogonality_threshold", 0.49))
|
|
227
|
+
removesmalllinkstrsh = float(
|
|
228
|
+
opts.get("smalllink_threshold", DEFAULT_SMALLLINK_THRESHOLD)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# A few short recovery cycles are enough in practice and avoid over-smoothing.
|
|
232
|
+
for _ in range(post_iter):
|
|
233
|
+
_, max_c_now, n_small_now = dual_criteria_on_fan_mesh(
|
|
234
|
+
np.asarray(vert_out, dtype=np.float64),
|
|
235
|
+
tria_out,
|
|
236
|
+
tri_origin_face_id_for_dual,
|
|
237
|
+
quad_face_mask_for_dual,
|
|
238
|
+
cosphi_threshold=cosphi_threshold,
|
|
239
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
240
|
+
jsferic=jsferic,
|
|
241
|
+
)
|
|
242
|
+
if (float(max_c_now) <= cosphi_threshold + 1.0e-9) and (
|
|
243
|
+
int(n_small_now) == 0
|
|
244
|
+
):
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
# Same compact logs as the main ortho+merge loop: each orthogonalize pass
|
|
248
|
+
# visits every zone once — without this, [ZONE] lines look like a hang.
|
|
249
|
+
ortho_res = orthogonalize_tria_mesh(
|
|
250
|
+
np.asarray(vert_out, dtype=np.float64),
|
|
251
|
+
np.asarray(tria_out, dtype=np.int64),
|
|
252
|
+
cosphi_threshold=cosphi_threshold,
|
|
253
|
+
removesmalllinkstrsh=removesmalllinkstrsh,
|
|
254
|
+
buffer_layers=int(opts.get("buffer_layers", 2)),
|
|
255
|
+
max_global_iter=int(
|
|
256
|
+
opts.get(
|
|
257
|
+
"max_global_iter",
|
|
258
|
+
int(opts.get("inner_iter", 4)) + 2,
|
|
259
|
+
)
|
|
260
|
+
),
|
|
261
|
+
smooth_iter=int(
|
|
262
|
+
opts.get("smooth_iter", int(opts.get("inner_iter", 4)) * 4)
|
|
263
|
+
),
|
|
264
|
+
enable_edge_flips=bool(opts.get("enable_edge_flips", True)),
|
|
265
|
+
verbose=False,
|
|
266
|
+
jsferic=jsferic,
|
|
267
|
+
fixed=fixed,
|
|
268
|
+
)
|
|
269
|
+
vert_out = ortho_res.vert
|
|
270
|
+
tria_out = ortho_res.tria
|
|
271
|
+
|
|
272
|
+
return np.asarray(vert_out, dtype=np.float64), conn, tria_out, tnum_out
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def smood(
|
|
276
|
+
vert=None, conn=None, tria=None, tnum=None, opts=None, hfun=None, harg=[],
|
|
277
|
+
fixed=None,
|
|
278
|
+
):
|
|
279
|
+
"""Smooth a mesh with orthogonalization for flow simulations.
|
|
280
|
+
|
|
281
|
+
Combine orthogonalization (aspect-ratio control) with hill-climbing
|
|
282
|
+
smoothing (angle optimization) via the ortho-merge pipeline.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
vert : ndarray of shape (V, 2), optional
|
|
287
|
+
Vertex coordinates.
|
|
288
|
+
conn : ndarray of shape (E, 2), optional
|
|
289
|
+
Constrained edges.
|
|
290
|
+
tria : ndarray of shape (T, 3), optional
|
|
291
|
+
Triangle connectivity.
|
|
292
|
+
tnum : ndarray of shape (T, 1), optional
|
|
293
|
+
Part index per triangle.
|
|
294
|
+
opts : dict, optional
|
|
295
|
+
Pipeline options (defaults via :func:`makeopt_smood`):
|
|
296
|
+
|
|
297
|
+
- ``vtol`` : float, default ``1.0e-3`` — vertex movement tolerance
|
|
298
|
+
- ``iter`` : int, default ``4`` — outer ortho/merge cycles
|
|
299
|
+
- ``inner_iter`` : int, default ``4`` — inner iterations per cycle
|
|
300
|
+
- ``ortho_factor`` : float, default ``0.5`` — ortho vs. smooth blend
|
|
301
|
+
- ``relaxation`` : float, default ``0.75`` — coordinate update relaxation
|
|
302
|
+
- ``orthogonality_threshold`` : float, default ``0.49`` — max ``|cos φ|``
|
|
303
|
+
- ``smalllink_threshold`` : float, default ``0.11`` — small flow-link threshold
|
|
304
|
+
- ``require_both_criteria`` : bool, default ``False`` — enforce dual criteria
|
|
305
|
+
- ``enforce_output_dual_criteria`` : bool — post-pass on final triangles
|
|
306
|
+
- ``post_output_ortho_iter`` : int, default ``3`` — post-recovery cycles
|
|
307
|
+
- ``max_recovery_iterations`` : int, default ``100`` — recovery cycle cap
|
|
308
|
+
- ``recovery_stagnation_break`` : int, default ``10`` — stagnation stop
|
|
309
|
+
- ``preserve_merged_quads`` : bool, default ``False`` — keep quad faces
|
|
310
|
+
- ``spherical`` : bool, default ``False`` — lon/lat vs. planar geometry
|
|
311
|
+
- ``merge_small_links`` : bool, default ``False`` — merge vs. flip-only mode
|
|
312
|
+
- ``disp`` : int or float, default ``4`` — progress interval; ``np.inf`` for quiet
|
|
313
|
+
hfun : callable, optional
|
|
314
|
+
Mesh-size function (reserved for future use).
|
|
315
|
+
harg : tuple, optional
|
|
316
|
+
Extra arguments for ``hfun``.
|
|
317
|
+
fixed : array_like of int, optional
|
|
318
|
+
Indices (into ``vert``) of vertices to hold fixed: they are never
|
|
319
|
+
displaced by orthogonalization or smoothing. Node numbering is
|
|
320
|
+
preserved by the pipeline, so the indices stay valid throughout.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
vert : ndarray of shape (V, 2)
|
|
325
|
+
Updated vertex coordinates.
|
|
326
|
+
conn : ndarray of shape (E, 2)
|
|
327
|
+
Updated constrained edges.
|
|
328
|
+
tria : ndarray of shape (T, 3)
|
|
329
|
+
Updated triangle connectivity (or mixed face rows if quads preserved).
|
|
330
|
+
tnum : ndarray of shape (T, 1)
|
|
331
|
+
Updated part indices.
|
|
332
|
+
|
|
333
|
+
Notes
|
|
334
|
+
-----
|
|
335
|
+
Delegates to :mod:`bluemesh2d.ortho_merge.ortho_merge_iter`. See
|
|
336
|
+
:mod:`bluemesh2d.ortho_merge.orthogonalize` for MeshKernel / Delft3D-FM
|
|
337
|
+
references.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
if vert is None:
|
|
341
|
+
vert = np.empty((0, 2))
|
|
342
|
+
if conn is None:
|
|
343
|
+
conn = np.empty((0, 2), dtype=int)
|
|
344
|
+
if tria is None:
|
|
345
|
+
tria = np.empty((0, 3), dtype=int)
|
|
346
|
+
if tnum is None:
|
|
347
|
+
tnum = np.empty((0, 1), dtype=int)
|
|
348
|
+
if opts is None:
|
|
349
|
+
opts = {}
|
|
350
|
+
|
|
351
|
+
opts = makeopt_smood(opts)
|
|
352
|
+
|
|
353
|
+
if conn.size == 0:
|
|
354
|
+
edge, _ = tricon(tria)
|
|
355
|
+
ebnd = edge[:, 3] < 1 # use boundary edge
|
|
356
|
+
conn = edge[ebnd, 0:2]
|
|
357
|
+
|
|
358
|
+
if tnum.size == 0:
|
|
359
|
+
tnum = np.ones((tria.shape[0], 1), dtype=int)
|
|
360
|
+
|
|
361
|
+
if not (
|
|
362
|
+
isinstance(vert, np.ndarray)
|
|
363
|
+
and isinstance(conn, np.ndarray)
|
|
364
|
+
and isinstance(tria, np.ndarray)
|
|
365
|
+
and isinstance(tnum, np.ndarray)
|
|
366
|
+
and isinstance(opts, dict)
|
|
367
|
+
):
|
|
368
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
369
|
+
|
|
370
|
+
nvrt = vert.shape[0]
|
|
371
|
+
|
|
372
|
+
if fixed is not None:
|
|
373
|
+
fixed = np.unique(np.asarray(fixed, dtype=int).ravel())
|
|
374
|
+
if fixed.size and (fixed.min() < 0 or fixed.max() >= nvrt):
|
|
375
|
+
raise ValueError("smood:invalidInputs - Invalid FIXED input array.")
|
|
376
|
+
|
|
377
|
+
if np.min(conn[:, :2]) < 0 or np.max(conn[:, :2]) > nvrt:
|
|
378
|
+
raise ValueError("smood:invalidInputs - Invalid CONN input array.")
|
|
379
|
+
|
|
380
|
+
if np.min(tria[:, :3]) < 0 or np.max(tria[:, :3]) > nvrt:
|
|
381
|
+
raise ValueError("smood:invalidInputs - Invalid TRIA input array.")
|
|
382
|
+
|
|
383
|
+
if not np.isinf(opts["disp"]):
|
|
384
|
+
print("\n Smooth triangulation for Delft3D-FM computation...\n")
|
|
385
|
+
|
|
386
|
+
return _ortho_merge_pipeline(vert, conn, tria, tnum, opts, fixed=fixed)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def makeopt_smood(opts=None):
|
|
390
|
+
"""Set up and validate the options dictionary for :func:`smood`.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
opts : dict or None, optional
|
|
395
|
+
User options; if ``None``, start from an empty dict.
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
opts : dict
|
|
400
|
+
Validated options dictionary.
|
|
401
|
+
"""
|
|
402
|
+
if opts is None:
|
|
403
|
+
opts = {}
|
|
404
|
+
|
|
405
|
+
if "iter" not in opts:
|
|
406
|
+
# Default pipeline: 4 ortho <-> merge outer cycles.
|
|
407
|
+
opts["iter"] = 4
|
|
408
|
+
else:
|
|
409
|
+
if not isinstance(opts["iter"], (int, float)):
|
|
410
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
411
|
+
if opts["iter"] <= 0:
|
|
412
|
+
raise ValueError("smood:invalidOptionValues - Invalid OPT.ITER selection.")
|
|
413
|
+
|
|
414
|
+
if "inner_iter" not in opts:
|
|
415
|
+
opts["inner_iter"] = 4
|
|
416
|
+
else:
|
|
417
|
+
if not isinstance(opts["inner_iter"], (int, float)):
|
|
418
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
419
|
+
if opts["inner_iter"] <= 0:
|
|
420
|
+
raise ValueError(
|
|
421
|
+
"smood:invalidOptionValues - Invalid OPT.INNER_ITER selection."
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if "ortho_factor" not in opts:
|
|
425
|
+
opts["ortho_factor"] = 0.5
|
|
426
|
+
else:
|
|
427
|
+
if not isinstance(opts["ortho_factor"], (int, float)):
|
|
428
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
429
|
+
if not (0.0 <= opts["ortho_factor"] <= 1.0):
|
|
430
|
+
raise ValueError(
|
|
431
|
+
"smood:invalidOptionValues - ORTHO_FACTOR must be in [0, 1]."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if "relaxation" not in opts:
|
|
435
|
+
opts["relaxation"] = 0.75
|
|
436
|
+
else:
|
|
437
|
+
if not isinstance(opts["relaxation"], (int, float)):
|
|
438
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
439
|
+
if not (0.0 < opts["relaxation"] <= 1.0):
|
|
440
|
+
raise ValueError(
|
|
441
|
+
"smood:invalidOptionValues - RELAXATION must be in (0, 1]."
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
if "disp" not in opts:
|
|
445
|
+
opts["disp"] = 8
|
|
446
|
+
else:
|
|
447
|
+
if not isinstance(opts["disp"], (int, float)):
|
|
448
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
449
|
+
if opts["disp"] <= 0:
|
|
450
|
+
raise ValueError("smood:invalidOptionValues - Invalid OPT.DISP selection.")
|
|
451
|
+
|
|
452
|
+
if "vtol" not in opts:
|
|
453
|
+
opts["vtol"] = 1.0e-3
|
|
454
|
+
else:
|
|
455
|
+
if not isinstance(opts["vtol"], (int, float)):
|
|
456
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
457
|
+
if opts["vtol"] <= 0:
|
|
458
|
+
raise ValueError("smood:invalidOptionValues - Invalid OPT.VTOL selection.")
|
|
459
|
+
|
|
460
|
+
if "dbug" not in opts:
|
|
461
|
+
opts["dbug"] = False
|
|
462
|
+
else:
|
|
463
|
+
if not isinstance(opts["dbug"], bool):
|
|
464
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
465
|
+
|
|
466
|
+
if "orthogonality_threshold" not in opts:
|
|
467
|
+
opts["orthogonality_threshold"] = 0.49 # max|cosphi|
|
|
468
|
+
else:
|
|
469
|
+
if not isinstance(opts["orthogonality_threshold"], (int, float)):
|
|
470
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
471
|
+
if not (0.0 <= opts["orthogonality_threshold"] <= 1.0):
|
|
472
|
+
raise ValueError(
|
|
473
|
+
"smood:invalidOptionValues - ORTHOGONALITY_THRESHOLD must be in [0, 1]."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if "smalllink_threshold" not in opts:
|
|
477
|
+
opts["smalllink_threshold"] = DEFAULT_SMALLLINK_THRESHOLD
|
|
478
|
+
else:
|
|
479
|
+
if not isinstance(opts["smalllink_threshold"], (int, float)):
|
|
480
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
481
|
+
opts["smalllink_threshold"] = float(opts["smalllink_threshold"])
|
|
482
|
+
|
|
483
|
+
if "buffer_layers" not in opts:
|
|
484
|
+
opts["buffer_layers"] = 2
|
|
485
|
+
else:
|
|
486
|
+
if not isinstance(opts["buffer_layers"], (int, float)):
|
|
487
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
488
|
+
opts["buffer_layers"] = int(opts["buffer_layers"])
|
|
489
|
+
if opts["buffer_layers"] <= 0:
|
|
490
|
+
raise ValueError("smood:invalidOptionValues - buffer_layers must be > 0.")
|
|
491
|
+
|
|
492
|
+
if "enable_edge_flips" not in opts:
|
|
493
|
+
opts["enable_edge_flips"] = True
|
|
494
|
+
else:
|
|
495
|
+
if not isinstance(opts["enable_edge_flips"], bool):
|
|
496
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
497
|
+
|
|
498
|
+
if "max_global_iter" not in opts:
|
|
499
|
+
opts["max_global_iter"] = int(opts["inner_iter"]) + 2
|
|
500
|
+
else:
|
|
501
|
+
if not isinstance(opts["max_global_iter"], (int, float)):
|
|
502
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
503
|
+
opts["max_global_iter"] = int(opts["max_global_iter"])
|
|
504
|
+
if opts["max_global_iter"] <= 0:
|
|
505
|
+
raise ValueError("smood:invalidOptionValues - max_global_iter must be > 0.")
|
|
506
|
+
|
|
507
|
+
if "smooth_iter" not in opts:
|
|
508
|
+
opts["smooth_iter"] = int(opts["inner_iter"]) * 4
|
|
509
|
+
else:
|
|
510
|
+
if not isinstance(opts["smooth_iter"], (int, float)):
|
|
511
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
512
|
+
opts["smooth_iter"] = int(opts["smooth_iter"])
|
|
513
|
+
if opts["smooth_iter"] <= 0:
|
|
514
|
+
raise ValueError("smood:invalidOptionValues - smooth_iter must be > 0.")
|
|
515
|
+
|
|
516
|
+
if "stop_if_no_merge" not in opts:
|
|
517
|
+
opts["stop_if_no_merge"] = False
|
|
518
|
+
else:
|
|
519
|
+
if not isinstance(opts["stop_if_no_merge"], bool):
|
|
520
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
521
|
+
|
|
522
|
+
# REQUIRE_BOTH_CRITERIA (fan-proxy dual check + recovery)
|
|
523
|
+
if "require_both_criteria" not in opts:
|
|
524
|
+
opts["require_both_criteria"] = DEFAULT_REQUIRE_STRICT_DUAL
|
|
525
|
+
else:
|
|
526
|
+
if not isinstance(opts["require_both_criteria"], bool):
|
|
527
|
+
raise TypeError(
|
|
528
|
+
"smood:incorrectInputClass - require_both_criteria must be bool."
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
if "max_recovery_iterations" not in opts:
|
|
532
|
+
opts["max_recovery_iterations"] = 100
|
|
533
|
+
else:
|
|
534
|
+
if not isinstance(opts["max_recovery_iterations"], (int, float)):
|
|
535
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
536
|
+
opts["max_recovery_iterations"] = int(opts["max_recovery_iterations"])
|
|
537
|
+
if opts["max_recovery_iterations"] < 0:
|
|
538
|
+
raise ValueError(
|
|
539
|
+
"smood:invalidOptionValues - max_recovery_iterations must be >= 0."
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if "recovery_stagnation_break" not in opts:
|
|
543
|
+
opts["recovery_stagnation_break"] = 10
|
|
544
|
+
else:
|
|
545
|
+
if not isinstance(opts["recovery_stagnation_break"], (int, float)):
|
|
546
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
547
|
+
opts["recovery_stagnation_break"] = int(opts["recovery_stagnation_break"])
|
|
548
|
+
if opts["recovery_stagnation_break"] < 0:
|
|
549
|
+
raise ValueError(
|
|
550
|
+
"smood:invalidOptionValues - recovery_stagnation_break must be >= 0."
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
if "outer_stagnation_break" not in opts:
|
|
554
|
+
opts["outer_stagnation_break"] = 2
|
|
555
|
+
else:
|
|
556
|
+
if not isinstance(opts["outer_stagnation_break"], (int, float)):
|
|
557
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
558
|
+
opts["outer_stagnation_break"] = int(opts["outer_stagnation_break"])
|
|
559
|
+
if opts["outer_stagnation_break"] < 0:
|
|
560
|
+
raise ValueError(
|
|
561
|
+
"smood:invalidOptionValues - outer_stagnation_break must be >= 0."
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if "adaptive_recovery" not in opts:
|
|
565
|
+
opts["adaptive_recovery"] = True
|
|
566
|
+
else:
|
|
567
|
+
if not isinstance(opts["adaptive_recovery"], bool):
|
|
568
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
569
|
+
|
|
570
|
+
if "recovery_buffer_growth" not in opts:
|
|
571
|
+
opts["recovery_buffer_growth"] = 1
|
|
572
|
+
else:
|
|
573
|
+
if not isinstance(opts["recovery_buffer_growth"], (int, float)):
|
|
574
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
575
|
+
opts["recovery_buffer_growth"] = int(opts["recovery_buffer_growth"])
|
|
576
|
+
if opts["recovery_buffer_growth"] < 0:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
"smood:invalidOptionValues - recovery_buffer_growth must be >= 0."
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
if "recovery_smooth_iter_growth" not in opts:
|
|
582
|
+
opts["recovery_smooth_iter_growth"] = 6
|
|
583
|
+
else:
|
|
584
|
+
if not isinstance(opts["recovery_smooth_iter_growth"], (int, float)):
|
|
585
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
586
|
+
opts["recovery_smooth_iter_growth"] = int(opts["recovery_smooth_iter_growth"])
|
|
587
|
+
if opts["recovery_smooth_iter_growth"] < 0:
|
|
588
|
+
raise ValueError(
|
|
589
|
+
"smood:invalidOptionValues - recovery_smooth_iter_growth must be >= 0."
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
if "recovery_global_iter_growth" not in opts:
|
|
593
|
+
opts["recovery_global_iter_growth"] = 1
|
|
594
|
+
else:
|
|
595
|
+
if not isinstance(opts["recovery_global_iter_growth"], (int, float)):
|
|
596
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
597
|
+
opts["recovery_global_iter_growth"] = int(opts["recovery_global_iter_growth"])
|
|
598
|
+
if opts["recovery_global_iter_growth"] < 0:
|
|
599
|
+
raise ValueError(
|
|
600
|
+
"smood:invalidOptionValues - recovery_global_iter_growth must be >= 0."
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
if "preserve_merged_quads" not in opts:
|
|
604
|
+
opts["preserve_merged_quads"] = False
|
|
605
|
+
else:
|
|
606
|
+
if not isinstance(opts["preserve_merged_quads"], bool):
|
|
607
|
+
raise TypeError("smood:incorrectInputClass - Incorrect input class.")
|
|
608
|
+
|
|
609
|
+
if "spherical" not in opts:
|
|
610
|
+
opts["spherical"] = False
|
|
611
|
+
else:
|
|
612
|
+
if not isinstance(opts["spherical"], bool):
|
|
613
|
+
raise TypeError("smood:incorrectInputClass - spherical must be bool.")
|
|
614
|
+
|
|
615
|
+
if "merge_small_links" not in opts:
|
|
616
|
+
opts["merge_small_links"] = False
|
|
617
|
+
else:
|
|
618
|
+
if not isinstance(opts["merge_small_links"], bool):
|
|
619
|
+
raise TypeError(
|
|
620
|
+
"smood:incorrectInputClass - merge_small_links must be bool."
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
return opts
|