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.
Files changed (74) hide show
  1. bluemesh2d/_version.py +24 -0
  2. bluemesh2d/aabb_tree/findball.py +121 -0
  3. bluemesh2d/aabb_tree/findtria.py +299 -0
  4. bluemesh2d/aabb_tree/maketree.py +147 -0
  5. bluemesh2d/aabb_tree/mapvert.py +72 -0
  6. bluemesh2d/aabb_tree/queryset.py +83 -0
  7. bluemesh2d/aabb_tree/scantree.py +124 -0
  8. bluemesh2d/dependencies.py +83 -0
  9. bluemesh2d/feedback.py +142 -0
  10. bluemesh2d/geom_util/boundary_util.py +216 -0
  11. bluemesh2d/geom_util/getiso.py +194 -0
  12. bluemesh2d/geom_util/poly_util.py +795 -0
  13. bluemesh2d/geom_util/proj_util.py +250 -0
  14. bluemesh2d/geomesh_util/border_util.py +283 -0
  15. bluemesh2d/geomesh_util/depth_field.py +333 -0
  16. bluemesh2d/geomesh_util/grd_util.py +1000 -0
  17. bluemesh2d/geomesh_util/interpolation_mesh.py +318 -0
  18. bluemesh2d/geomesh_util/merge_circumcenters.py +268 -0
  19. bluemesh2d/geomesh_util/water_polygon.py +406 -0
  20. bluemesh2d/hfun_util/build_hfun.py +589 -0
  21. bluemesh2d/hfun_util/hfun_dispersion.py +96 -0
  22. bluemesh2d/hfun_util/lfshfn.py +110 -0
  23. bluemesh2d/hfun_util/limhfn.py +65 -0
  24. bluemesh2d/hfun_util/make_constant_hfun.py +32 -0
  25. bluemesh2d/hfun_util/make_depth_hfun.py +51 -0
  26. bluemesh2d/hfun_util/smooth_and_precomput.py +188 -0
  27. bluemesh2d/hfun_util/trihfn.py +84 -0
  28. bluemesh2d/hjac_util/limgrad.py +105 -0
  29. bluemesh2d/mesh_ball/cdtbal1.py +38 -0
  30. bluemesh2d/mesh_ball/cdtbal2.py +104 -0
  31. bluemesh2d/mesh_ball/inv_2x2.py +61 -0
  32. bluemesh2d/mesh_ball/inv_3x3.py +72 -0
  33. bluemesh2d/mesh_ball/pwrbal2.py +134 -0
  34. bluemesh2d/mesh_ball/tribal2.py +27 -0
  35. bluemesh2d/mesh_cost/relhfn.py +62 -0
  36. bluemesh2d/mesh_cost/triang.py +59 -0
  37. bluemesh2d/mesh_cost/triarea.py +51 -0
  38. bluemesh2d/mesh_cost/trideg.py +44 -0
  39. bluemesh2d/mesh_cost/triscr.py +43 -0
  40. bluemesh2d/mesh_file/bnd_util.py +664 -0
  41. bluemesh2d/mesh_file/loadmsh.py +165 -0
  42. bluemesh2d/mesh_file/ugrid.py +308 -0
  43. bluemesh2d/mesh_util/cfmtri.py +118 -0
  44. bluemesh2d/mesh_util/deltri.py +131 -0
  45. bluemesh2d/mesh_util/idxtri.py +53 -0
  46. bluemesh2d/mesh_util/isfeat.py +90 -0
  47. bluemesh2d/mesh_util/minlen.py +46 -0
  48. bluemesh2d/mesh_util/setset.py +49 -0
  49. bluemesh2d/mesh_util/tricon.py +90 -0
  50. bluemesh2d/mesh_util/tridiv.py +187 -0
  51. bluemesh2d/meshgen.py +202 -0
  52. bluemesh2d/ortho_merge/constants.py +27 -0
  53. bluemesh2d/ortho_merge/geometry.py +64 -0
  54. bluemesh2d/ortho_merge/ortho_merge_iter.py +812 -0
  55. bluemesh2d/ortho_merge/orthogonalize.py +2989 -0
  56. bluemesh2d/pipeline.py +277 -0
  57. bluemesh2d/poly_data/airfoil.msh +958 -0
  58. bluemesh2d/poly_data/channel.msh +212 -0
  59. bluemesh2d/poly_data/islands.msh +13819 -0
  60. bluemesh2d/poly_data/lake.msh +612 -0
  61. bluemesh2d/poly_data/river.msh +690 -0
  62. bluemesh2d/poly_test/inpoly.py +105 -0
  63. bluemesh2d/poly_test/inpoly_mat.py +104 -0
  64. bluemesh2d/refine.py +972 -0
  65. bluemesh2d/smood.py +623 -0
  66. bluemesh2d/smooth.py +522 -0
  67. bluemesh2d/tricost.py +426 -0
  68. bluemesh2d/tridemo.py +723 -0
  69. bluemesh2d/triread.py +53 -0
  70. bluemesh2d-0.1.1.dev0.dist-info/METADATA +139 -0
  71. bluemesh2d-0.1.1.dev0.dist-info/RECORD +74 -0
  72. bluemesh2d-0.1.1.dev0.dist-info/WHEEL +5 -0
  73. bluemesh2d-0.1.1.dev0.dist-info/licenses/LICENSE +674 -0
  74. 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